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:
@@ -0,0 +1,182 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { Plus, Pencil, Trash2, Search, PenTool, Stamp, Type } 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 { 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 { SignatureAsset, SignatureAssetType } from '../types';
|
||||
import { useSignatures } from '../hooks/use-signatures';
|
||||
|
||||
const TYPE_LABELS: Record<SignatureAssetType, string> = {
|
||||
signature: 'Semnătură', stamp: 'Ștampilă', initials: 'Inițiale',
|
||||
};
|
||||
|
||||
const TYPE_ICONS: Record<SignatureAssetType, typeof PenTool> = {
|
||||
signature: PenTool, stamp: Stamp, initials: Type,
|
||||
};
|
||||
|
||||
type ViewMode = 'list' | 'add' | 'edit';
|
||||
|
||||
export function DigitalSignaturesModule() {
|
||||
const { assets, allAssets, loading, filters, updateFilter, addAsset, updateAsset, removeAsset } = useSignatures();
|
||||
const [viewMode, setViewMode] = useState<ViewMode>('list');
|
||||
const [editingAsset, setEditingAsset] = useState<SignatureAsset | null>(null);
|
||||
|
||||
const handleSubmit = async (data: Omit<SignatureAsset, 'id' | 'createdAt'>) => {
|
||||
if (viewMode === 'edit' && editingAsset) {
|
||||
await updateAsset(editingAsset.id, data);
|
||||
} else {
|
||||
await addAsset(data);
|
||||
}
|
||||
setViewMode('list');
|
||||
setEditingAsset(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">{allAssets.length}</p></CardContent></Card>
|
||||
{(Object.keys(TYPE_LABELS) as SignatureAssetType[]).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">{allAssets.filter((a) => a.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ă..." value={filters.search} onChange={(e) => updateFilter('search', e.target.value)} className="pl-9" />
|
||||
</div>
|
||||
<Select value={filters.type} onValueChange={(v) => updateFilter('type', v as SignatureAssetType | 'all')}>
|
||||
<SelectTrigger className="w-[150px]"><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">Toate tipurile</SelectItem>
|
||||
{(Object.keys(TYPE_LABELS) as SignatureAssetType[]).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>
|
||||
) : assets.length === 0 ? (
|
||||
<p className="py-8 text-center text-sm text-muted-foreground">Niciun element găsit. Adaugă o semnătură, ștampilă sau inițiale.</p>
|
||||
) : (
|
||||
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{assets.map((asset) => {
|
||||
const Icon = TYPE_ICONS[asset.type];
|
||||
return (
|
||||
<Card key={asset.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={() => { setEditingAsset(asset); setViewMode('edit'); }}>
|
||||
<Pencil className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon" className="h-7 w-7 text-destructive" onClick={() => removeAsset(asset.id)}>
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex h-12 w-12 items-center justify-center rounded-lg border bg-muted/30">
|
||||
{asset.imageUrl ? (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img src={asset.imageUrl} alt={asset.label} className="max-h-10 max-w-10 object-contain" />
|
||||
) : (
|
||||
<Icon className="h-6 w-6 text-muted-foreground" />
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium">{asset.label}</p>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Badge variant="outline" className="text-[10px]">{TYPE_LABELS[asset.type]}</Badge>
|
||||
<span className="text-xs text-muted-foreground">{asset.owner}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{(viewMode === 'add' || viewMode === 'edit') && (
|
||||
<Card>
|
||||
<CardHeader><CardTitle>{viewMode === 'edit' ? 'Editare' : 'Element nou'}</CardTitle></CardHeader>
|
||||
<CardContent>
|
||||
<AssetForm initial={editingAsset ?? undefined} onSubmit={handleSubmit} onCancel={() => { setViewMode('list'); setEditingAsset(null); }} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function AssetForm({ initial, onSubmit, onCancel }: {
|
||||
initial?: SignatureAsset;
|
||||
onSubmit: (data: Omit<SignatureAsset, 'id' | 'createdAt'>) => void;
|
||||
onCancel: () => void;
|
||||
}) {
|
||||
const [label, setLabel] = useState(initial?.label ?? '');
|
||||
const [type, setType] = useState<SignatureAssetType>(initial?.type ?? 'signature');
|
||||
const [imageUrl, setImageUrl] = useState(initial?.imageUrl ?? '');
|
||||
const [owner, setOwner] = useState(initial?.owner ?? '');
|
||||
const [company, setCompany] = useState<CompanyId>(initial?.company ?? 'beletage');
|
||||
|
||||
return (
|
||||
<form onSubmit={(e) => { e.preventDefault(); onSubmit({ label, type, imageUrl, owner, company, tags: initial?.tags ?? [], visibility: initial?.visibility ?? 'all' }); }} className="space-y-4">
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<div><Label>Denumire</Label><Input value={label} onChange={(e) => setLabel(e.target.value)} className="mt-1" required /></div>
|
||||
<div><Label>Tip</Label>
|
||||
<Select value={type} onValueChange={(v) => setType(v as SignatureAssetType)}>
|
||||
<SelectTrigger className="mt-1"><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="signature">Semnătură</SelectItem>
|
||||
<SelectItem value="stamp">Ștampilă</SelectItem>
|
||||
<SelectItem value="initials">Inițiale</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<div><Label>Proprietar</Label><Input value={owner} onChange={(e) => setOwner(e.target.value)} className="mt-1" /></div>
|
||||
<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>
|
||||
<div>
|
||||
<Label>URL imagine</Label>
|
||||
<Input value={imageUrl} onChange={(e) => setImageUrl(e.target.value)} className="mt-1" placeholder="https://... sau data:image/png;base64,..." />
|
||||
<p className="mt-1 text-xs text-muted-foreground">URL către imaginea semnăturii/ștampilei. Suportă URL-uri externe sau base64.</p>
|
||||
</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
src/modules/digital-signatures/config.ts
Normal file
17
src/modules/digital-signatures/config.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import type { ModuleConfig } from '@/core/module-registry/types';
|
||||
|
||||
export const digitalSignaturesConfig: ModuleConfig = {
|
||||
id: 'digital-signatures',
|
||||
name: 'Semnături și Ștampile',
|
||||
description: 'Gestionare semnături digitale, ștampile și inițiale pentru documente',
|
||||
icon: 'pen-tool',
|
||||
route: '/digital-signatures',
|
||||
category: 'management',
|
||||
featureFlag: 'module.digital-signatures',
|
||||
visibility: 'all',
|
||||
version: '0.1.0',
|
||||
dependencies: [],
|
||||
storageNamespace: 'digital-signatures',
|
||||
navOrder: 30,
|
||||
tags: ['semnături', 'ștampile', 'documente'],
|
||||
};
|
||||
73
src/modules/digital-signatures/hooks/use-signatures.ts
Normal file
73
src/modules/digital-signatures/hooks/use-signatures.ts
Normal file
@@ -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 { SignatureAsset, SignatureAssetType } from '../types';
|
||||
|
||||
const PREFIX = 'sig:';
|
||||
|
||||
export interface SignatureFilters {
|
||||
search: string;
|
||||
type: SignatureAssetType | 'all';
|
||||
}
|
||||
|
||||
export function useSignatures() {
|
||||
const storage = useStorage('digital-signatures');
|
||||
const [assets, setAssets] = useState<SignatureAsset[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [filters, setFilters] = useState<SignatureFilters>({ search: '', type: 'all' });
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
setLoading(true);
|
||||
const keys = await storage.list();
|
||||
const results: SignatureAsset[] = [];
|
||||
for (const key of keys) {
|
||||
if (key.startsWith(PREFIX)) {
|
||||
const item = await storage.get<SignatureAsset>(key);
|
||||
if (item) results.push(item);
|
||||
}
|
||||
}
|
||||
results.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
|
||||
setAssets(results);
|
||||
setLoading(false);
|
||||
}, [storage]);
|
||||
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||
useEffect(() => { refresh(); }, [refresh]);
|
||||
|
||||
const addAsset = useCallback(async (data: Omit<SignatureAsset, 'id' | 'createdAt'>) => {
|
||||
const asset: SignatureAsset = { ...data, id: uuid(), createdAt: new Date().toISOString() };
|
||||
await storage.set(`${PREFIX}${asset.id}`, asset);
|
||||
await refresh();
|
||||
return asset;
|
||||
}, [storage, refresh]);
|
||||
|
||||
const updateAsset = useCallback(async (id: string, updates: Partial<SignatureAsset>) => {
|
||||
const existing = assets.find((a) => a.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, assets]);
|
||||
|
||||
const removeAsset = useCallback(async (id: string) => {
|
||||
await storage.delete(`${PREFIX}${id}`);
|
||||
await refresh();
|
||||
}, [storage, refresh]);
|
||||
|
||||
const updateFilter = useCallback(<K extends keyof SignatureFilters>(key: K, value: SignatureFilters[K]) => {
|
||||
setFilters((prev) => ({ ...prev, [key]: value }));
|
||||
}, []);
|
||||
|
||||
const filteredAssets = assets.filter((a) => {
|
||||
if (filters.type !== 'all' && a.type !== filters.type) return false;
|
||||
if (filters.search) {
|
||||
const q = filters.search.toLowerCase();
|
||||
return a.label.toLowerCase().includes(q) || a.owner.toLowerCase().includes(q);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
return { assets: filteredAssets, allAssets: assets, loading, filters, updateFilter, addAsset, updateAsset, removeAsset, refresh };
|
||||
}
|
||||
3
src/modules/digital-signatures/index.ts
Normal file
3
src/modules/digital-signatures/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { digitalSignaturesConfig } from './config';
|
||||
export { DigitalSignaturesModule } from './components/digital-signatures-module';
|
||||
export type { SignatureAsset, SignatureAssetType } from './types';
|
||||
16
src/modules/digital-signatures/types.ts
Normal file
16
src/modules/digital-signatures/types.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import type { Visibility } from '@/core/module-registry/types';
|
||||
import type { CompanyId } from '@/core/auth/types';
|
||||
|
||||
export type SignatureAssetType = 'signature' | 'stamp' | 'initials';
|
||||
|
||||
export interface SignatureAsset {
|
||||
id: string;
|
||||
label: string;
|
||||
type: SignatureAssetType;
|
||||
imageUrl: string;
|
||||
owner: string;
|
||||
company: CompanyId;
|
||||
tags: string[];
|
||||
visibility: Visibility;
|
||||
createdAt: string;
|
||||
}
|
||||
Reference in New Issue
Block a user