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,168 @@
'use client';
import { useState } from 'react';
import { Plus, Pencil, Trash2, Search, Mail, Phone, MapPin } 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 { AddressContact, ContactType } from '../types';
import { useContacts } from '../hooks/use-contacts';
const TYPE_LABELS: Record<ContactType, string> = {
client: 'Client', supplier: 'Furnizor', institution: 'Instituție', collaborator: 'Colaborator',
};
type ViewMode = 'list' | 'add' | 'edit';
export function AddressBookModule() {
const { contacts, allContacts, loading, filters, updateFilter, addContact, updateContact, removeContact } = useContacts();
const [viewMode, setViewMode] = useState<ViewMode>('list');
const [editingContact, setEditingContact] = useState<AddressContact | null>(null);
const handleSubmit = async (data: Omit<AddressContact, 'id' | 'createdAt'>) => {
if (viewMode === 'edit' && editingContact) {
await updateContact(editingContact.id, data);
} else {
await addContact(data);
}
setViewMode('list');
setEditingContact(null);
};
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</p><p className="text-2xl font-bold">{allContacts.length}</p></CardContent></Card>
{(Object.keys(TYPE_LABELS) as ContactType[]).slice(0, 3).map((type) => (
<Card key={type}><CardContent className="p-4">
<p className="text-xs text-muted-foreground">{TYPE_LABELS[type]}</p>
<p className="text-2xl font-bold">{allContacts.filter((c) => c.type === type).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ă contact..." value={filters.search} onChange={(e) => updateFilter('search', e.target.value)} className="pl-9" />
</div>
<Select value={filters.type} onValueChange={(v) => updateFilter('type', v as ContactType | 'all')}>
<SelectTrigger className="w-[150px]"><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem value="all">Toate tipurile</SelectItem>
{(Object.keys(TYPE_LABELS) as ContactType[]).map((t) => (
<SelectItem key={t} value={t}>{TYPE_LABELS[t]}</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>
) : contacts.length === 0 ? (
<p className="py-8 text-center text-sm text-muted-foreground">Niciun contact găsit.</p>
) : (
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
{contacts.map((contact) => (
<Card key={contact.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={() => { setEditingContact(contact); setViewMode('edit'); }}>
<Pencil className="h-3.5 w-3.5" />
</Button>
<Button variant="ghost" size="icon" className="h-7 w-7 text-destructive" onClick={() => removeContact(contact.id)}>
<Trash2 className="h-3.5 w-3.5" />
</Button>
</div>
<div className="space-y-2">
<div>
<p className="font-medium">{contact.name}</p>
<div className="flex items-center gap-2">
{contact.company && <p className="text-xs text-muted-foreground">{contact.company}</p>}
<Badge variant="outline" className="text-[10px]">{TYPE_LABELS[contact.type]}</Badge>
</div>
</div>
{contact.email && (
<div className="flex items-center gap-1.5 text-xs text-muted-foreground">
<Mail className="h-3 w-3" /><span>{contact.email}</span>
</div>
)}
{contact.phone && (
<div className="flex items-center gap-1.5 text-xs text-muted-foreground">
<Phone className="h-3 w-3" /><span>{contact.phone}</span>
</div>
)}
{contact.address && (
<div className="flex items-center gap-1.5 text-xs text-muted-foreground">
<MapPin className="h-3 w-3" /><span className="truncate">{contact.address}</span>
</div>
)}
</div>
</CardContent>
</Card>
))}
</div>
)}
</>
)}
{(viewMode === 'add' || viewMode === 'edit') && (
<Card>
<CardHeader><CardTitle>{viewMode === 'edit' ? 'Editare contact' : 'Contact nou'}</CardTitle></CardHeader>
<CardContent>
<ContactForm initial={editingContact ?? undefined} onSubmit={handleSubmit} onCancel={() => { setViewMode('list'); setEditingContact(null); }} />
</CardContent>
</Card>
)}
</div>
);
}
function ContactForm({ initial, onSubmit, onCancel }: {
initial?: AddressContact;
onSubmit: (data: Omit<AddressContact, 'id' | 'createdAt'>) => void;
onCancel: () => void;
}) {
const [name, setName] = useState(initial?.name ?? '');
const [company, setCompany] = useState(initial?.company ?? '');
const [type, setType] = useState<ContactType>(initial?.type ?? 'client');
const [email, setEmail] = useState(initial?.email ?? '');
const [phone, setPhone] = useState(initial?.phone ?? '');
const [address, setAddress] = useState(initial?.address ?? '');
const [notes, setNotes] = useState(initial?.notes ?? '');
return (
<form onSubmit={(e) => { e.preventDefault(); onSubmit({ name, company, type, email, phone, address, notes, tags: initial?.tags ?? [], visibility: initial?.visibility ?? 'all' }); }} className="space-y-4">
<div className="grid gap-4 sm:grid-cols-2">
<div><Label>Nume</Label><Input value={name} onChange={(e) => setName(e.target.value)} className="mt-1" required /></div>
<div><Label>Companie/Organizație</Label><Input value={company} onChange={(e) => setCompany(e.target.value)} className="mt-1" /></div>
</div>
<div className="grid gap-4 sm:grid-cols-3">
<div><Label>Tip</Label>
<Select value={type} onValueChange={(v) => setType(v as ContactType)}>
<SelectTrigger className="mt-1"><SelectValue /></SelectTrigger>
<SelectContent>{(Object.keys(TYPE_LABELS) as ContactType[]).map((t) => (<SelectItem key={t} value={t}>{TYPE_LABELS[t]}</SelectItem>))}</SelectContent>
</Select>
</div>
<div><Label>Email</Label><Input type="email" value={email} onChange={(e) => setEmail(e.target.value)} className="mt-1" /></div>
<div><Label>Telefon</Label><Input type="tel" value={phone} onChange={(e) => setPhone(e.target.value)} className="mt-1" /></div>
</div>
<div><Label>Adresă</Label><Input value={address} onChange={(e) => setAddress(e.target.value)} className="mt-1" /></div>
<div><Label>Note</Label><Textarea value={notes} onChange={(e) => setNotes(e.target.value)} rows={2} 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>
);
}
+17
View File
@@ -0,0 +1,17 @@
import type { ModuleConfig } from '@/core/module-registry/types';
export const addressBookConfig: ModuleConfig = {
id: 'address-book',
name: 'Contacte',
description: 'Agendă de contacte organizată pe tipuri: clienți, furnizori, instituții, colaboratori',
icon: 'users',
route: '/address-book',
category: 'management',
featureFlag: 'module.address-book',
visibility: 'all',
version: '0.1.0',
dependencies: [],
storageNamespace: 'address-book',
navOrder: 32,
tags: ['contacte', 'agendă', 'clienți'],
};
@@ -0,0 +1,73 @@
'use client';
import { useState, useEffect, useCallback } from 'react';
import { useStorage } from '@/core/storage';
import { v4 as uuid } from 'uuid';
import type { AddressContact, ContactType } from '../types';
const PREFIX = 'contact:';
export interface ContactFilters {
search: string;
type: ContactType | 'all';
}
export function useContacts() {
const storage = useStorage('address-book');
const [contacts, setContacts] = useState<AddressContact[]>([]);
const [loading, setLoading] = useState(true);
const [filters, setFilters] = useState<ContactFilters>({ search: '', type: 'all' });
const refresh = useCallback(async () => {
setLoading(true);
const keys = await storage.list();
const results: AddressContact[] = [];
for (const key of keys) {
if (key.startsWith(PREFIX)) {
const item = await storage.get<AddressContact>(key);
if (item) results.push(item);
}
}
results.sort((a, b) => a.name.localeCompare(b.name));
setContacts(results);
setLoading(false);
}, [storage]);
// eslint-disable-next-line react-hooks/set-state-in-effect
useEffect(() => { refresh(); }, [refresh]);
const addContact = useCallback(async (data: Omit<AddressContact, 'id' | 'createdAt'>) => {
const contact: AddressContact = { ...data, id: uuid(), createdAt: new Date().toISOString() };
await storage.set(`${PREFIX}${contact.id}`, contact);
await refresh();
return contact;
}, [storage, refresh]);
const updateContact = useCallback(async (id: string, updates: Partial<AddressContact>) => {
const existing = contacts.find((c) => c.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, contacts]);
const removeContact = useCallback(async (id: string) => {
await storage.delete(`${PREFIX}${id}`);
await refresh();
}, [storage, refresh]);
const updateFilter = useCallback(<K extends keyof ContactFilters>(key: K, value: ContactFilters[K]) => {
setFilters((prev) => ({ ...prev, [key]: value }));
}, []);
const filteredContacts = contacts.filter((c) => {
if (filters.type !== 'all' && c.type !== filters.type) return false;
if (filters.search) {
const q = filters.search.toLowerCase();
return c.name.toLowerCase().includes(q) || c.company.toLowerCase().includes(q) || c.email.toLowerCase().includes(q) || c.phone.includes(q);
}
return true;
});
return { contacts: filteredContacts, allContacts: contacts, loading, filters, updateFilter, addContact, updateContact, removeContact, refresh };
}
+3
View File
@@ -0,0 +1,3 @@
export { addressBookConfig } from './config';
export { AddressBookModule } from './components/address-book-module';
export type { AddressContact, ContactType } from './types';
+17
View File
@@ -0,0 +1,17 @@
import type { Visibility } from '@/core/module-registry/types';
export type ContactType = 'client' | 'supplier' | 'institution' | 'collaborator';
export interface AddressContact {
id: string;
name: string;
company: string;
type: ContactType;
email: string;
phone: string;
address: string;
tags: string[];
notes: string;
visibility: Visibility;
createdAt: string;
}