feat(registratura): rework with company-prefixed numbering, directions, deadlines, attachments

- Company-specific numbering (B-0001/2026, US-0001/2026, SDT-0001/2026)
- Direction: Intrat/Ieșit replaces old 3-way type
- 9 document types: Contract, Ofertă, Factură, Scrisoare, etc.
- Status simplified to Deschis/Închis with cascade close for linked entries
- Address Book autocomplete for sender/recipient
- Deadline tracking with overdue day counter
- File attachment support (base64 encoding)
- Linked entries system

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Marius Tarau
2026-02-18 06:35:23 +02:00
parent 84d9db4515
commit 98eda56035
8 changed files with 550 additions and 109 deletions
@@ -1,9 +1,10 @@
'use client';
import { Pencil, Trash2 } from 'lucide-react';
import { Pencil, Trash2, CheckCircle2, Link2 } from 'lucide-react';
import { Button } from '@/shared/components/ui/button';
import { Badge } from '@/shared/components/ui/badge';
import type { RegistryEntry } from '../types';
import type { RegistryEntry, DocumentType } from '../types';
import { getOverdueDays } from '../services/registry-service';
import { cn } from '@/shared/lib/utils';
interface RegistryTableProps {
@@ -11,29 +12,32 @@ interface RegistryTableProps {
loading: boolean;
onEdit: (entry: RegistryEntry) => void;
onDelete: (id: string) => void;
onClose: (id: string) => void;
}
const TYPE_LABELS: Record<string, string> = {
incoming: 'Intrare',
outgoing: 'Ieșire',
internal: 'Intern',
const DIRECTION_LABELS: Record<string, string> = {
intrat: 'Intrat',
iesit: 'Ieșit',
};
const DOC_TYPE_LABELS: Record<DocumentType, string> = {
contract: 'Contract',
oferta: 'Ofertă',
factura: 'Factură',
scrisoare: 'Scrisoare',
aviz: 'Aviz',
'nota-de-comanda': 'Notă comandă',
raport: 'Raport',
cerere: 'Cerere',
altele: 'Altele',
};
const STATUS_LABELS: Record<string, string> = {
registered: 'Înregistrat',
'in-progress': 'În lucru',
completed: 'Finalizat',
archived: 'Arhivat',
deschis: 'Deschis',
inchis: 'Închis',
};
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) {
export function RegistryTable({ entries, loading, onEdit, onDelete, onClose }: RegistryTableProps) {
if (loading) {
return <p className="py-8 text-center text-sm text-muted-foreground">Se încarcă...</p>;
}
@@ -53,40 +57,90 @@ export function RegistryTable({ entries, loading, onEdit, onDelete }: RegistryTa
<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">Dir.</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">Termen</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>
))}
{entries.map((entry) => {
const overdueDays = entry.status === 'deschis' ? getOverdueDays(entry.deadline) : null;
const isOverdue = overdueDays !== null && overdueDays > 0;
return (
<tr
key={entry.id}
className={cn(
'border-b transition-colors hover:bg-muted/20',
isOverdue && 'bg-destructive/5'
)}
>
<td className="px-3 py-2 font-mono text-xs whitespace-nowrap">{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={entry.direction === 'intrat' ? 'default' : 'secondary'}
className="text-xs"
>
{DIRECTION_LABELS[entry.direction]}
</Badge>
</td>
<td className="px-3 py-2 text-xs">{DOC_TYPE_LABELS[entry.documentType]}</td>
<td className="px-3 py-2 max-w-[200px] truncate">
{entry.subject}
{entry.linkedEntryIds.length > 0 && (
<Link2 className="ml-1 inline h-3 w-3 text-muted-foreground" />
)}
{entry.attachments.length > 0 && (
<Badge variant="outline" className="ml-1 text-[10px] px-1">
{entry.attachments.length} fișiere
</Badge>
)}
</td>
<td className="px-3 py-2 max-w-[130px] truncate">{entry.sender}</td>
<td className="px-3 py-2 max-w-[130px] truncate">{entry.recipient}</td>
<td className="px-3 py-2 text-xs whitespace-nowrap">
{entry.deadline ? (
<span className={cn(isOverdue && 'font-medium text-destructive')}>
{formatDate(entry.deadline)}
{overdueDays !== null && overdueDays > 0 && (
<span className="ml-1 text-[10px]">({overdueDays}z depășit)</span>
)}
{overdueDays !== null && overdueDays < 0 && (
<span className="ml-1 text-[10px] text-muted-foreground">({Math.abs(overdueDays)}z)</span>
)}
</span>
) : (
<span className="text-muted-foreground"></span>
)}
</td>
<td className="px-3 py-2">
<Badge variant={entry.status === 'deschis' ? 'default' : 'outline'}>
{STATUS_LABELS[entry.status]}
</Badge>
</td>
<td className="px-3 py-2 text-right">
<div className="flex justify-end gap-1">
{entry.status === 'deschis' && (
<Button variant="ghost" size="icon" className="h-7 w-7 text-green-600" onClick={() => onClose(entry.id)} title="Închide">
<CheckCircle2 className="h-3.5 w-3.5" />
</Button>
)}
<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>