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,204 @@
'use client';
import { useState } from 'react';
import { Plus, Pencil, Trash2, Search } 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 { InventoryItem, InventoryItemType, InventoryItemStatus } from '../types';
import { useInventory } from '../hooks/use-inventory';
const TYPE_LABELS: Record<InventoryItemType, string> = {
laptop: 'Laptop', desktop: 'Desktop', monitor: 'Monitor', printer: 'Imprimantă',
phone: 'Telefon', tablet: 'Tabletă', network: 'Rețea', peripheral: 'Periferic', other: 'Altele',
};
const STATUS_LABELS: Record<InventoryItemStatus, string> = {
active: 'Activ', 'in-repair': 'În reparație', storage: 'Depozitat', decommissioned: 'Dezafectat',
};
type ViewMode = 'list' | 'add' | 'edit';
export function ItInventoryModule() {
const { items, allItems, loading, filters, updateFilter, addItem, updateItem, removeItem } = useInventory();
const [viewMode, setViewMode] = useState<ViewMode>('list');
const [editingItem, setEditingItem] = useState<InventoryItem | null>(null);
const handleSubmit = async (data: Omit<InventoryItem, 'id' | 'createdAt'>) => {
if (viewMode === 'edit' && editingItem) {
await updateItem(editingItem.id, data);
} else {
await addItem(data);
}
setViewMode('list');
setEditingItem(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">{allItems.length}</p></CardContent></Card>
<Card><CardContent className="p-4"><p className="text-xs text-muted-foreground">Active</p><p className="text-2xl font-bold">{allItems.filter((i) => i.status === 'active').length}</p></CardContent></Card>
<Card><CardContent className="p-4"><p className="text-xs text-muted-foreground">În reparație</p><p className="text-2xl font-bold">{allItems.filter((i) => i.status === 'in-repair').length}</p></CardContent></Card>
<Card><CardContent className="p-4"><p className="text-xs text-muted-foreground">Dezafectate</p><p className="text-2xl font-bold">{allItems.filter((i) => i.status === 'decommissioned').length}</p></CardContent></Card>
</div>
{viewMode === 'list' && (
<>
{/* Filters */}
<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ă..." value={filters.search} onChange={(e) => updateFilter('search', e.target.value)} className="pl-9" />
</div>
<Select value={filters.type} onValueChange={(v) => updateFilter('type', v as InventoryItemType | 'all')}>
<SelectTrigger className="w-[140px]"><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem value="all">Toate tipurile</SelectItem>
{(Object.keys(TYPE_LABELS) as InventoryItemType[]).map((t) => (
<SelectItem key={t} value={t}>{TYPE_LABELS[t]}</SelectItem>
))}
</SelectContent>
</Select>
<Select value={filters.status} onValueChange={(v) => updateFilter('status', v as InventoryItemStatus | 'all')}>
<SelectTrigger className="w-[140px]"><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem value="all">Toate</SelectItem>
{(Object.keys(STATUS_LABELS) as InventoryItemStatus[]).map((s) => (
<SelectItem key={s} value={s}>{STATUS_LABELS[s]}</SelectItem>
))}
</SelectContent>
</Select>
<Button onClick={() => setViewMode('add')} className="shrink-0">
<Plus className="mr-1.5 h-4 w-4" /> Adaugă
</Button>
</div>
{/* Table */}
{loading ? (
<p className="py-8 text-center text-sm text-muted-foreground">Se încarcă...</p>
) : items.length === 0 ? (
<p className="py-8 text-center text-sm text-muted-foreground">Niciun echipament găsit.</p>
) : (
<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">Nume</th>
<th className="px-3 py-2 text-left font-medium">Tip</th>
<th className="px-3 py-2 text-left font-medium">S/N</th>
<th className="px-3 py-2 text-left font-medium">Atribuit</th>
<th className="px-3 py-2 text-left font-medium">Locație</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>
{items.map((item) => (
<tr key={item.id} className="border-b hover:bg-muted/20 transition-colors">
<td className="px-3 py-2 font-medium">{item.name}</td>
<td className="px-3 py-2"><Badge variant="outline">{TYPE_LABELS[item.type]}</Badge></td>
<td className="px-3 py-2 font-mono text-xs">{item.serialNumber}</td>
<td className="px-3 py-2">{item.assignedTo}</td>
<td className="px-3 py-2">{item.location}</td>
<td className="px-3 py-2"><Badge variant="secondary">{STATUS_LABELS[item.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={() => { setEditingItem(item); setViewMode('edit'); }}>
<Pencil className="h-3.5 w-3.5" />
</Button>
<Button variant="ghost" size="icon" className="h-7 w-7 text-destructive" onClick={() => removeItem(item.id)}>
<Trash2 className="h-3.5 w-3.5" />
</Button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</>
)}
{(viewMode === 'add' || viewMode === 'edit') && (
<Card>
<CardHeader><CardTitle>{viewMode === 'edit' ? 'Editare echipament' : 'Echipament nou'}</CardTitle></CardHeader>
<CardContent>
<InventoryForm
initial={editingItem ?? undefined}
onSubmit={handleSubmit}
onCancel={() => { setViewMode('list'); setEditingItem(null); }}
/>
</CardContent>
</Card>
)}
</div>
);
}
function InventoryForm({ initial, onSubmit, onCancel }: {
initial?: InventoryItem;
onSubmit: (data: Omit<InventoryItem, 'id' | 'createdAt'>) => void;
onCancel: () => void;
}) {
const [name, setName] = useState(initial?.name ?? '');
const [type, setType] = useState<InventoryItemType>(initial?.type ?? 'laptop');
const [serialNumber, setSerialNumber] = useState(initial?.serialNumber ?? '');
const [assignedTo, setAssignedTo] = useState(initial?.assignedTo ?? '');
const [company, setCompany] = useState<CompanyId>(initial?.company ?? 'beletage');
const [location, setLocation] = useState(initial?.location ?? '');
const [purchaseDate, setPurchaseDate] = useState(initial?.purchaseDate ?? '');
const [status, setStatus] = useState<InventoryItemStatus>(initial?.status ?? 'active');
const [notes, setNotes] = useState(initial?.notes ?? '');
return (
<form onSubmit={(e) => { e.preventDefault(); onSubmit({ name, type, serialNumber, assignedTo, company, location, purchaseDate, status, notes, tags: initial?.tags ?? [], visibility: initial?.visibility ?? 'all' }); }} className="space-y-4">
<div className="grid gap-4 sm:grid-cols-2">
<div><Label>Nume echipament</Label><Input value={name} onChange={(e) => setName(e.target.value)} className="mt-1" required /></div>
<div><Label>Tip</Label>
<Select value={type} onValueChange={(v) => setType(v as InventoryItemType)}>
<SelectTrigger className="mt-1"><SelectValue /></SelectTrigger>
<SelectContent>{(Object.keys(TYPE_LABELS) as InventoryItemType[]).map((t) => (<SelectItem key={t} value={t}>{TYPE_LABELS[t]}</SelectItem>))}</SelectContent>
</Select>
</div>
</div>
<div className="grid gap-4 sm:grid-cols-2">
<div><Label>Număr serie</Label><Input value={serialNumber} onChange={(e) => setSerialNumber(e.target.value)} className="mt-1" /></div>
<div><Label>Atribuit</Label><Input value={assignedTo} onChange={(e) => setAssignedTo(e.target.value)} className="mt-1" /></div>
</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>Locație</Label><Input value={location} onChange={(e) => setLocation(e.target.value)} className="mt-1" /></div>
<div><Label>Data achiziție</Label><Input type="date" value={purchaseDate} onChange={(e) => setPurchaseDate(e.target.value)} className="mt-1" /></div>
</div>
<div className="grid gap-4 sm:grid-cols-2">
<div><Label>Status</Label>
<Select value={status} onValueChange={(v) => setStatus(v as InventoryItemStatus)}>
<SelectTrigger className="mt-1"><SelectValue /></SelectTrigger>
<SelectContent>{(Object.keys(STATUS_LABELS) as InventoryItemStatus[]).map((s) => (<SelectItem key={s} value={s}>{STATUS_LABELS[s]}</SelectItem>))}</SelectContent>
</Select>
</div>
<div><Label>Note</Label><Textarea value={notes} onChange={(e) => setNotes(e.target.value)} rows={2} className="mt-1" /></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 itInventoryConfig: ModuleConfig = {
id: 'it-inventory',
name: 'Inventar IT',
description: 'Evidență echipamente IT cu urmărire atribuiri și locații',
icon: 'monitor',
route: '/it-inventory',
category: 'management',
featureFlag: 'module.it-inventory',
visibility: 'all',
version: '0.1.0',
dependencies: [],
storageNamespace: 'it-inventory',
navOrder: 31,
tags: ['inventar', 'echipamente', 'IT'],
};
@@ -0,0 +1,79 @@
'use client';
import { useState, useEffect, useCallback } from 'react';
import { useStorage } from '@/core/storage';
import { v4 as uuid } from 'uuid';
import type { InventoryItem, InventoryItemType, InventoryItemStatus } from '../types';
const PREFIX = 'item:';
export interface InventoryFilters {
search: string;
type: InventoryItemType | 'all';
status: InventoryItemStatus | 'all';
company: string;
}
export function useInventory() {
const storage = useStorage('it-inventory');
const [items, setItems] = useState<InventoryItem[]>([]);
const [loading, setLoading] = useState(true);
const [filters, setFilters] = useState<InventoryFilters>({
search: '', type: 'all', status: 'all', company: 'all',
});
const refresh = useCallback(async () => {
setLoading(true);
const keys = await storage.list();
const results: InventoryItem[] = [];
for (const key of keys) {
if (key.startsWith(PREFIX)) {
const item = await storage.get<InventoryItem>(key);
if (item) results.push(item);
}
}
results.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
setItems(results);
setLoading(false);
}, [storage]);
// eslint-disable-next-line react-hooks/set-state-in-effect
useEffect(() => { refresh(); }, [refresh]);
const addItem = useCallback(async (data: Omit<InventoryItem, 'id' | 'createdAt'>) => {
const item: InventoryItem = { ...data, id: uuid(), createdAt: new Date().toISOString() };
await storage.set(`${PREFIX}${item.id}`, item);
await refresh();
return item;
}, [storage, refresh]);
const updateItem = useCallback(async (id: string, updates: Partial<InventoryItem>) => {
const existing = items.find((i) => i.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, items]);
const removeItem = useCallback(async (id: string) => {
await storage.delete(`${PREFIX}${id}`);
await refresh();
}, [storage, refresh]);
const updateFilter = useCallback(<K extends keyof InventoryFilters>(key: K, value: InventoryFilters[K]) => {
setFilters((prev) => ({ ...prev, [key]: value }));
}, []);
const filteredItems = items.filter((item) => {
if (filters.type !== 'all' && item.type !== filters.type) return false;
if (filters.status !== 'all' && item.status !== filters.status) return false;
if (filters.company !== 'all' && item.company !== filters.company) return false;
if (filters.search) {
const q = filters.search.toLowerCase();
return item.name.toLowerCase().includes(q) || item.serialNumber.toLowerCase().includes(q) || item.assignedTo.toLowerCase().includes(q);
}
return true;
});
return { items: filteredItems, allItems: items, loading, filters, updateFilter, addItem, updateItem, removeItem, refresh };
}
+3
View File
@@ -0,0 +1,3 @@
export { itInventoryConfig } from './config';
export { ItInventoryModule } from './components/it-inventory-module';
export type { InventoryItem, InventoryItemType, InventoryItemStatus } from './types';
+35
View File
@@ -0,0 +1,35 @@
import type { Visibility } from '@/core/module-registry/types';
import type { CompanyId } from '@/core/auth/types';
export type InventoryItemType =
| 'laptop'
| 'desktop'
| 'monitor'
| 'printer'
| 'phone'
| 'tablet'
| 'network'
| 'peripheral'
| 'other';
export type InventoryItemStatus =
| 'active'
| 'in-repair'
| 'storage'
| 'decommissioned';
export interface InventoryItem {
id: string;
name: string;
type: InventoryItemType;
serialNumber: string;
assignedTo: string;
company: CompanyId;
location: string;
purchaseDate: string;
status: InventoryItemStatus;
tags: string[];
notes: string;
visibility: Visibility;
createdAt: string;
}