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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user