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,128 @@
'use client';
import { useState } from 'react';
import { Plus } from 'lucide-react';
import { Button } from '@/shared/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/shared/components/ui/card';
import { Badge } from '@/shared/components/ui/badge';
import { useRegistry } from '../hooks/use-registry';
import { RegistryFilters } from './registry-filters';
import { RegistryTable } from './registry-table';
import { RegistryEntryForm } from './registry-entry-form';
import type { RegistryEntry } from '../types';
type ViewMode = 'list' | 'add' | 'edit';
export function RegistraturaModule() {
const {
entries, allEntries, loading, filters, updateFilter,
addEntry, updateEntry, removeEntry,
} = useRegistry();
const [viewMode, setViewMode] = useState<ViewMode>('list');
const [editingEntry, setEditingEntry] = useState<RegistryEntry | null>(null);
const handleAdd = async (data: Omit<RegistryEntry, 'id' | 'number' | 'createdAt' | 'updatedAt'>) => {
await addEntry(data);
setViewMode('list');
};
const handleEdit = (entry: RegistryEntry) => {
setEditingEntry(entry);
setViewMode('edit');
};
const handleUpdate = async (data: Omit<RegistryEntry, 'id' | 'number' | 'createdAt' | 'updatedAt'>) => {
if (!editingEntry) return;
await updateEntry(editingEntry.id, data);
setEditingEntry(null);
setViewMode('list');
};
const handleDelete = async (id: string) => {
await removeEntry(id);
};
const handleCancel = () => {
setViewMode('list');
setEditingEntry(null);
};
// Stats
const total = allEntries.length;
const incoming = allEntries.filter((e) => e.type === 'incoming').length;
const outgoing = allEntries.filter((e) => e.type === 'outgoing').length;
const inProgress = allEntries.filter((e) => e.status === 'in-progress').length;
return (
<div className="space-y-6">
{/* Stats */}
<div className="grid grid-cols-2 gap-3 sm:grid-cols-4">
<StatCard label="Total" value={total} />
<StatCard label="Intrare" value={incoming} />
<StatCard label="Ieșire" value={outgoing} />
<StatCard label="În lucru" value={inProgress} />
</div>
{viewMode === 'list' && (
<>
<div className="flex items-center justify-between gap-4">
<RegistryFilters filters={filters} onUpdate={updateFilter} />
<Button onClick={() => setViewMode('add')} className="shrink-0">
<Plus className="mr-1.5 h-4 w-4" /> Adaugă
</Button>
</div>
<RegistryTable
entries={entries}
loading={loading}
onEdit={handleEdit}
onDelete={handleDelete}
/>
{!loading && (
<p className="text-xs text-muted-foreground">
{entries.length} din {total} înregistrări afișate
</p>
)}
</>
)}
{viewMode === 'add' && (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
Înregistrare nouă
<Badge variant="outline" className="text-xs">Nr. auto</Badge>
</CardTitle>
</CardHeader>
<CardContent>
<RegistryEntryForm onSubmit={handleAdd} onCancel={handleCancel} />
</CardContent>
</Card>
)}
{viewMode === 'edit' && editingEntry && (
<Card>
<CardHeader>
<CardTitle>Editare {editingEntry.number}</CardTitle>
</CardHeader>
<CardContent>
<RegistryEntryForm initial={editingEntry} onSubmit={handleUpdate} onCancel={handleCancel} />
</CardContent>
</Card>
)}
</div>
);
}
function StatCard({ label, value }: { label: string; value: number }) {
return (
<Card>
<CardContent className="p-4">
<p className="text-xs text-muted-foreground">{label}</p>
<p className="text-2xl font-bold">{value}</p>
</CardContent>
</Card>
);
}
@@ -0,0 +1,118 @@
'use client';
import { useState } from 'react';
import type { CompanyId } from '@/core/auth/types';
import type { RegistryEntry, RegistryEntryType, RegistryEntryStatus } from '../types';
import { Input } from '@/shared/components/ui/input';
import { Label } from '@/shared/components/ui/label';
import { Textarea } from '@/shared/components/ui/textarea';
import { Button } from '@/shared/components/ui/button';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/shared/components/ui/select';
interface RegistryEntryFormProps {
initial?: RegistryEntry;
onSubmit: (data: Omit<RegistryEntry, 'id' | 'number' | 'createdAt' | 'updatedAt'>) => void;
onCancel: () => void;
}
export function RegistryEntryForm({ initial, onSubmit, onCancel }: RegistryEntryFormProps) {
const [type, setType] = useState<RegistryEntryType>(initial?.type ?? 'incoming');
const [subject, setSubject] = useState(initial?.subject ?? '');
const [date, setDate] = useState(initial?.date ?? new Date().toISOString().slice(0, 10));
const [sender, setSender] = useState(initial?.sender ?? '');
const [recipient, setRecipient] = useState(initial?.recipient ?? '');
const [company, setCompany] = useState<CompanyId>(initial?.company ?? 'beletage');
const [status, setStatus] = useState<RegistryEntryStatus>(initial?.status ?? 'registered');
const [notes, setNotes] = useState(initial?.notes ?? '');
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
onSubmit({
type,
subject,
date,
sender,
recipient,
company,
status,
notes,
tags: initial?.tags ?? [],
visibility: initial?.visibility ?? 'all',
});
};
return (
<form onSubmit={handleSubmit} className="space-y-4">
<div className="grid gap-4 sm:grid-cols-2">
<div>
<Label>Tip document</Label>
<Select value={type} onValueChange={(v) => setType(v as RegistryEntryType)}>
<SelectTrigger className="mt-1"><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem value="incoming">Intrare</SelectItem>
<SelectItem value="outgoing">Ieșire</SelectItem>
<SelectItem value="internal">Intern</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Label>Data</Label>
<Input type="date" value={date} onChange={(e) => setDate(e.target.value)} className="mt-1" />
</div>
</div>
<div>
<Label>Subiect</Label>
<Input value={subject} onChange={(e) => setSubject(e.target.value)} className="mt-1" required />
</div>
<div className="grid gap-4 sm:grid-cols-2">
<div>
<Label>Expeditor</Label>
<Input value={sender} onChange={(e) => setSender(e.target.value)} className="mt-1" />
</div>
<div>
<Label>Destinatar</Label>
<Input value={recipient} onChange={(e) => setRecipient(e.target.value)} className="mt-1" />
</div>
</div>
<div className="grid gap-4 sm:grid-cols-2">
<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>Status</Label>
<Select value={status} onValueChange={(v) => setStatus(v as RegistryEntryStatus)}>
<SelectTrigger className="mt-1"><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem value="registered">Înregistrat</SelectItem>
<SelectItem value="in-progress">În lucru</SelectItem>
<SelectItem value="completed">Finalizat</SelectItem>
<SelectItem value="archived">Arhivat</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div>
<Label>Note</Label>
<Textarea value={notes} onChange={(e) => setNotes(e.target.value)} rows={3} className="mt-1" />
</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>
);
}
@@ -0,0 +1,65 @@
'use client';
import { Search } from 'lucide-react';
import { Input } from '@/shared/components/ui/input';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/shared/components/ui/select';
import type { RegistryFilters as Filters } from '../hooks/use-registry';
interface RegistryFiltersProps {
filters: Filters;
onUpdate: <K extends keyof Filters>(key: K, value: Filters[K]) => void;
}
export function RegistryFilters({ filters, onUpdate }: RegistryFiltersProps) {
return (
<div className="flex flex-wrap items-center gap-3">
<div className="relative flex-1 min-w-[200px]">
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Caută după subiect, expeditor, număr..."
value={filters.search}
onChange={(e) => onUpdate('search', e.target.value)}
className="pl-9"
/>
</div>
<Select value={filters.type} onValueChange={(v) => onUpdate('type', v as Filters['type'])}>
<SelectTrigger className="w-[150px]">
<SelectValue placeholder="Tip" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">Toate tipurile</SelectItem>
<SelectItem value="incoming">Intrare</SelectItem>
<SelectItem value="outgoing">Ieșire</SelectItem>
<SelectItem value="internal">Intern</SelectItem>
</SelectContent>
</Select>
<Select value={filters.status} onValueChange={(v) => onUpdate('status', v as Filters['status'])}>
<SelectTrigger className="w-[150px]">
<SelectValue placeholder="Status" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">Toate</SelectItem>
<SelectItem value="registered">Înregistrat</SelectItem>
<SelectItem value="in-progress">În lucru</SelectItem>
<SelectItem value="completed">Finalizat</SelectItem>
<SelectItem value="archived">Arhivat</SelectItem>
</SelectContent>
</Select>
<Select value={filters.company} onValueChange={(v) => onUpdate('company', v)}>
<SelectTrigger className="w-[170px]">
<SelectValue placeholder="Companie" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">Toate companiile</SelectItem>
<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>
);
}
@@ -0,0 +1,102 @@
'use client';
import { Pencil, Trash2 } from 'lucide-react';
import { Button } from '@/shared/components/ui/button';
import { Badge } from '@/shared/components/ui/badge';
import type { RegistryEntry } from '../types';
import { cn } from '@/shared/lib/utils';
interface RegistryTableProps {
entries: RegistryEntry[];
loading: boolean;
onEdit: (entry: RegistryEntry) => void;
onDelete: (id: string) => void;
}
const TYPE_LABELS: Record<string, string> = {
incoming: 'Intrare',
outgoing: 'Ieșire',
internal: 'Intern',
};
const STATUS_LABELS: Record<string, string> = {
registered: 'Înregistrat',
'in-progress': 'În lucru',
completed: 'Finalizat',
archived: 'Arhivat',
};
const STATUS_VARIANT: Record<string, 'default' | 'secondary' | 'outline' | 'destructive'> = {
registered: 'default',
'in-progress': 'secondary',
completed: 'outline',
archived: 'outline',
};
export function RegistryTable({ entries, loading, onEdit, onDelete }: RegistryTableProps) {
if (loading) {
return <p className="py-8 text-center text-sm text-muted-foreground">Se încarcă...</p>;
}
if (entries.length === 0) {
return (
<p className="py-8 text-center text-sm text-muted-foreground">
Nicio înregistrare găsită. Adaugă prima înregistrare.
</p>
);
}
return (
<div className="overflow-x-auto rounded-lg border">
<table className="w-full text-sm">
<thead>
<tr className="border-b bg-muted/40">
<th className="px-3 py-2 text-left font-medium">Nr.</th>
<th className="px-3 py-2 text-left font-medium">Data</th>
<th className="px-3 py-2 text-left font-medium">Tip</th>
<th className="px-3 py-2 text-left font-medium">Subiect</th>
<th className="px-3 py-2 text-left font-medium">Expeditor</th>
<th className="px-3 py-2 text-left font-medium">Destinatar</th>
<th className="px-3 py-2 text-left font-medium">Status</th>
<th className="px-3 py-2 text-right font-medium">Acțiuni</th>
</tr>
</thead>
<tbody>
{entries.map((entry) => (
<tr key={entry.id} className={cn('border-b hover:bg-muted/20 transition-colors')}>
<td className="px-3 py-2 font-mono text-xs">{entry.number}</td>
<td className="px-3 py-2 text-xs whitespace-nowrap">{formatDate(entry.date)}</td>
<td className="px-3 py-2">
<Badge variant="outline" className="text-xs">{TYPE_LABELS[entry.type]}</Badge>
</td>
<td className="px-3 py-2 max-w-[250px] truncate">{entry.subject}</td>
<td className="px-3 py-2 max-w-[150px] truncate">{entry.sender}</td>
<td className="px-3 py-2 max-w-[150px] truncate">{entry.recipient}</td>
<td className="px-3 py-2">
<Badge variant={STATUS_VARIANT[entry.status]}>{STATUS_LABELS[entry.status]}</Badge>
</td>
<td className="px-3 py-2 text-right">
<div className="flex justify-end gap-1">
<Button variant="ghost" size="icon" className="h-7 w-7" onClick={() => onEdit(entry)}>
<Pencil className="h-3.5 w-3.5" />
</Button>
<Button variant="ghost" size="icon" className="h-7 w-7 text-destructive" onClick={() => onDelete(entry.id)}>
<Trash2 className="h-3.5 w-3.5" />
</Button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
);
}
function formatDate(iso: string): string {
try {
return new Date(iso).toLocaleDateString('ro-RO', { day: '2-digit', month: '2-digit', year: 'numeric' });
} catch {
return iso;
}
}