Initial commit: ArchiTools modular dashboard platform

Complete Next.js 16 application with 13 fully implemented modules:
Email Signature, Word XML Generator, Registratura, Dashboard,
Tag Manager, IT Inventory, Address Book, Password Vault,
Mini Utilities, Prompt Generator, Digital Signatures,
Word Templates, and AI Chat.

Includes core platform systems (module registry, feature flags,
storage abstraction, i18n, theming, auth stub, tagging),
16 technical documentation files, Docker deployment config,
and legacy HTML tool reference.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Marius Tarau
2026-02-17 12:50:25 +02:00
commit 4c46e8bcdd
189 changed files with 33780 additions and 0 deletions
@@ -0,0 +1,172 @@
'use client';
import { useState } from 'react';
import { Plus, Pencil, Trash2, Search, FileText, ExternalLink } from 'lucide-react';
import { Button } from '@/shared/components/ui/button';
import { Input } from '@/shared/components/ui/input';
import { Label } from '@/shared/components/ui/label';
import { Textarea } from '@/shared/components/ui/textarea';
import { Badge } from '@/shared/components/ui/badge';
import { Card, CardContent, CardHeader, CardTitle } from '@/shared/components/ui/card';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/shared/components/ui/select';
import type { CompanyId } from '@/core/auth/types';
import type { WordTemplate } from '../types';
import { useTemplates } from '../hooks/use-templates';
const TEMPLATE_CATEGORIES = [
'Contract', 'Memoriu tehnic', 'Ofertă', 'Factură', 'Raport', 'Deviz', 'Proces-verbal', 'Altele',
];
type ViewMode = 'list' | 'add' | 'edit';
export function WordTemplatesModule() {
const { templates, allTemplates, allCategories, loading, filters, updateFilter, addTemplate, updateTemplate, removeTemplate } = useTemplates();
const [viewMode, setViewMode] = useState<ViewMode>('list');
const [editingTemplate, setEditingTemplate] = useState<WordTemplate | null>(null);
const handleSubmit = async (data: Omit<WordTemplate, 'id' | 'createdAt'>) => {
if (viewMode === 'edit' && editingTemplate) {
await updateTemplate(editingTemplate.id, data);
} else {
await addTemplate(data);
}
setViewMode('list');
setEditingTemplate(null);
};
const filterCategories = allCategories.length > 0 ? allCategories : TEMPLATE_CATEGORIES;
return (
<div className="space-y-6">
{/* Stats */}
<div className="grid grid-cols-2 gap-3 sm:grid-cols-4">
<Card><CardContent className="p-4"><p className="text-xs text-muted-foreground">Total șabloane</p><p className="text-2xl font-bold">{allTemplates.length}</p></CardContent></Card>
<Card><CardContent className="p-4"><p className="text-xs text-muted-foreground">Categorii</p><p className="text-2xl font-bold">{allCategories.length}</p></CardContent></Card>
<Card><CardContent className="p-4"><p className="text-xs text-muted-foreground">Beletage</p><p className="text-2xl font-bold">{allTemplates.filter((t) => t.company === 'beletage').length}</p></CardContent></Card>
<Card><CardContent className="p-4"><p className="text-xs text-muted-foreground">Urban Switch</p><p className="text-2xl font-bold">{allTemplates.filter((t) => t.company === 'urban-switch').length}</p></CardContent></Card>
</div>
{viewMode === 'list' && (
<>
<div className="flex flex-wrap items-center gap-3">
<div className="relative min-w-[200px] flex-1">
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
<Input placeholder="Caută șablon..." value={filters.search} onChange={(e) => updateFilter('search', e.target.value)} className="pl-9" />
</div>
<Select value={filters.category} onValueChange={(v) => updateFilter('category', v)}>
<SelectTrigger className="w-[160px]"><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem value="all">Toate</SelectItem>
{filterCategories.map((c) => (
<SelectItem key={c} value={c}>{c}</SelectItem>
))}
</SelectContent>
</Select>
<Button onClick={() => setViewMode('add')} className="shrink-0">
<Plus className="mr-1.5 h-4 w-4" /> Adaugă
</Button>
</div>
{loading ? (
<p className="py-8 text-center text-sm text-muted-foreground">Se încarcă...</p>
) : templates.length === 0 ? (
<p className="py-8 text-center text-sm text-muted-foreground">
Niciun șablon găsit. Adaugă primul șablon Word.
</p>
) : (
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
{templates.map((tpl) => (
<Card key={tpl.id} className="group relative">
<CardContent className="p-4">
<div className="absolute right-2 top-2 flex gap-1 opacity-0 transition-opacity group-hover:opacity-100">
<Button variant="ghost" size="icon" className="h-7 w-7" onClick={() => { setEditingTemplate(tpl); setViewMode('edit'); }}>
<Pencil className="h-3.5 w-3.5" />
</Button>
<Button variant="ghost" size="icon" className="h-7 w-7 text-destructive" onClick={() => removeTemplate(tpl.id)}>
<Trash2 className="h-3.5 w-3.5" />
</Button>
</div>
<div className="flex items-start gap-3">
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg border bg-muted/30">
<FileText className="h-5 w-5 text-muted-foreground" />
</div>
<div className="min-w-0">
<p className="font-medium">{tpl.name}</p>
{tpl.description && <p className="mt-0.5 text-xs text-muted-foreground line-clamp-2">{tpl.description}</p>}
<div className="mt-1.5 flex flex-wrap gap-1">
{tpl.category && <Badge variant="outline" className="text-[10px]">{tpl.category}</Badge>}
<Badge variant="secondary" className="text-[10px]">v{tpl.version}</Badge>
</div>
{tpl.fileUrl && (
<a href={tpl.fileUrl} target="_blank" rel="noopener noreferrer" className="mt-1 inline-flex items-center gap-1 text-xs text-primary hover:underline">
<ExternalLink className="h-3 w-3" /> Deschide fișier
</a>
)}
</div>
</div>
</CardContent>
</Card>
))}
</div>
)}
</>
)}
{(viewMode === 'add' || viewMode === 'edit') && (
<Card>
<CardHeader><CardTitle>{viewMode === 'edit' ? 'Editare șablon' : 'Șablon nou'}</CardTitle></CardHeader>
<CardContent>
<TemplateForm initial={editingTemplate ?? undefined} onSubmit={handleSubmit} onCancel={() => { setViewMode('list'); setEditingTemplate(null); }} />
</CardContent>
</Card>
)}
</div>
);
}
function TemplateForm({ initial, onSubmit, onCancel }: {
initial?: WordTemplate;
onSubmit: (data: Omit<WordTemplate, 'id' | 'createdAt'>) => void;
onCancel: () => void;
}) {
const [name, setName] = useState(initial?.name ?? '');
const [description, setDescription] = useState(initial?.description ?? '');
const [category, setCategory] = useState(initial?.category ?? 'Contract');
const [fileUrl, setFileUrl] = useState(initial?.fileUrl ?? '');
const [company, setCompany] = useState<CompanyId>(initial?.company ?? 'beletage');
const [version, setVersion] = useState(initial?.version ?? '1.0.0');
return (
<form onSubmit={(e) => { e.preventDefault(); onSubmit({ name, description, category, fileUrl, company, version, tags: initial?.tags ?? [], visibility: initial?.visibility ?? 'all' }); }} className="space-y-4">
<div className="grid gap-4 sm:grid-cols-2">
<div><Label>Nume șablon</Label><Input value={name} onChange={(e) => setName(e.target.value)} className="mt-1" required /></div>
<div><Label>Categorie</Label>
<Select value={category} onValueChange={setCategory}>
<SelectTrigger className="mt-1"><SelectValue /></SelectTrigger>
<SelectContent>{TEMPLATE_CATEGORIES.map((c) => (<SelectItem key={c} value={c}>{c}</SelectItem>))}</SelectContent>
</Select>
</div>
</div>
<div><Label>Descriere</Label><Textarea value={description} onChange={(e) => setDescription(e.target.value)} rows={2} className="mt-1" /></div>
<div className="grid gap-4 sm:grid-cols-3">
<div><Label>Companie</Label>
<Select value={company} onValueChange={(v) => setCompany(v as CompanyId)}>
<SelectTrigger className="mt-1"><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem value="beletage">Beletage</SelectItem>
<SelectItem value="urban-switch">Urban Switch</SelectItem>
<SelectItem value="studii-de-teren">Studii de Teren</SelectItem>
<SelectItem value="group">Grup</SelectItem>
</SelectContent>
</Select>
</div>
<div><Label>Versiune</Label><Input value={version} onChange={(e) => setVersion(e.target.value)} className="mt-1" /></div>
<div><Label>URL fișier</Label><Input value={fileUrl} onChange={(e) => setFileUrl(e.target.value)} className="mt-1" placeholder="https://..." /></div>
</div>
<div className="flex justify-end gap-2 pt-2">
<Button type="button" variant="outline" onClick={onCancel}>Anulează</Button>
<Button type="submit">{initial ? 'Actualizează' : 'Adaugă'}</Button>
</div>
</form>
);
}
+17
View File
@@ -0,0 +1,17 @@
import type { ModuleConfig } from '@/core/module-registry/types';
export const wordTemplatesConfig: ModuleConfig = {
id: 'word-templates',
name: 'Șabloane Word',
description: 'Bibliotecă de șabloane Word organizate pe categorii cu suport versionare',
icon: 'file-text',
route: '/word-templates',
category: 'generators',
featureFlag: 'module.word-templates',
visibility: 'all',
version: '0.1.0',
dependencies: [],
storageNamespace: 'word-templates',
navOrder: 22,
tags: ['word', 'șabloane', 'documente'],
};
@@ -0,0 +1,75 @@
'use client';
import { useState, useEffect, useCallback } from 'react';
import { useStorage } from '@/core/storage';
import { v4 as uuid } from 'uuid';
import type { WordTemplate } from '../types';
const PREFIX = 'tpl:';
export interface TemplateFilters {
search: string;
category: string;
}
export function useTemplates() {
const storage = useStorage('word-templates');
const [templates, setTemplates] = useState<WordTemplate[]>([]);
const [loading, setLoading] = useState(true);
const [filters, setFilters] = useState<TemplateFilters>({ search: '', category: 'all' });
const refresh = useCallback(async () => {
setLoading(true);
const keys = await storage.list();
const results: WordTemplate[] = [];
for (const key of keys) {
if (key.startsWith(PREFIX)) {
const item = await storage.get<WordTemplate>(key);
if (item) results.push(item);
}
}
results.sort((a, b) => a.name.localeCompare(b.name));
setTemplates(results);
setLoading(false);
}, [storage]);
// eslint-disable-next-line react-hooks/set-state-in-effect
useEffect(() => { refresh(); }, [refresh]);
const addTemplate = useCallback(async (data: Omit<WordTemplate, 'id' | 'createdAt'>) => {
const template: WordTemplate = { ...data, id: uuid(), createdAt: new Date().toISOString() };
await storage.set(`${PREFIX}${template.id}`, template);
await refresh();
return template;
}, [storage, refresh]);
const updateTemplate = useCallback(async (id: string, updates: Partial<WordTemplate>) => {
const existing = templates.find((t) => t.id === id);
if (!existing) return;
const updated = { ...existing, ...updates, id: existing.id, createdAt: existing.createdAt };
await storage.set(`${PREFIX}${id}`, updated);
await refresh();
}, [storage, refresh, templates]);
const removeTemplate = useCallback(async (id: string) => {
await storage.delete(`${PREFIX}${id}`);
await refresh();
}, [storage, refresh]);
const updateFilter = useCallback(<K extends keyof TemplateFilters>(key: K, value: TemplateFilters[K]) => {
setFilters((prev) => ({ ...prev, [key]: value }));
}, []);
const allCategories = [...new Set(templates.map((t) => t.category).filter(Boolean))];
const filteredTemplates = templates.filter((t) => {
if (filters.category !== 'all' && t.category !== filters.category) return false;
if (filters.search) {
const q = filters.search.toLowerCase();
return t.name.toLowerCase().includes(q) || t.description.toLowerCase().includes(q);
}
return true;
});
return { templates: filteredTemplates, allTemplates: templates, allCategories, loading, filters, updateFilter, addTemplate, updateTemplate, removeTemplate, refresh };
}
+3
View File
@@ -0,0 +1,3 @@
export { wordTemplatesConfig } from './config';
export { WordTemplatesModule } from './components/word-templates-module';
export type { WordTemplate } from './types';
+15
View File
@@ -0,0 +1,15 @@
import type { Visibility } from '@/core/module-registry/types';
import type { CompanyId } from '@/core/auth/types';
export interface WordTemplate {
id: string;
name: string;
description: string;
category: string;
fileUrl: string;
company: CompanyId;
tags: string[];
version: string;
visibility: Visibility;
createdAt: string;
}