feat(registratura): 3.02 bidirectional integration, simplified status, threads

- Dynamic document types: string-based DocumentType synced with Tag Manager
  (new types auto-create tags under 'document-type' category)
- Added default types: 'Apel telefonic', 'Videoconferinta'
- Bidirectional Address Book: quick-create contacts from sender/recipient/
  assignee fields via QuickContactDialog popup
- Simplified status: Switch toggle replaces dropdown (default open)
- Responsabil (Assignee) field with contact autocomplete (ERP-ready)
- Entry threads: threadParentId links entries as replies, ThreadView shows
  parent/current/children tree with branching support
- Info tooltips on deadline, status, and assignee fields
- New Resp. column and thread icon in registry table
- All changes backward-compatible with existing data
This commit is contained in:
AI Assistant
2026-02-27 15:33:29 +02:00
parent b2618c041d
commit 2be0462e0d
10 changed files with 1199 additions and 273 deletions
@@ -1,11 +1,20 @@
'use client';
"use client";
import { Pencil, Trash2, CheckCircle2, Link2, Clock } from 'lucide-react';
import { Button } from '@/shared/components/ui/button';
import { Badge } from '@/shared/components/ui/badge';
import type { RegistryEntry, DocumentType } from '../types';
import { getOverdueDays } from '../services/registry-service';
import { cn } from '@/shared/lib/utils';
import {
Pencil,
Trash2,
CheckCircle2,
Link2,
Clock,
GitBranch,
User,
} from "lucide-react";
import { Button } from "@/shared/components/ui/button";
import { Badge } from "@/shared/components/ui/badge";
import type { RegistryEntry } from "../types";
import { DEFAULT_DOC_TYPE_LABELS } from "../types";
import { getOverdueDays } from "../services/registry-service";
import { cn } from "@/shared/lib/utils";
interface RegistryTableProps {
entries: RegistryEntry[];
@@ -16,30 +25,36 @@ interface RegistryTableProps {
}
const DIRECTION_LABELS: Record<string, string> = {
intrat: 'Intrat',
iesit: 'Ieșit',
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',
};
/** Resolve doc type label from defaults or capitalize custom type */
function getDocTypeLabel(type: string): string {
const label = DEFAULT_DOC_TYPE_LABELS[type];
if (label) return label;
// For custom types, capitalize first letter
return type.replace(/-/g, " ").replace(/^\w/, (c) => c.toUpperCase());
}
const STATUS_LABELS: Record<string, string> = {
deschis: 'Deschis',
inchis: 'Închis',
deschis: "Deschis",
inchis: "Închis",
};
export function RegistryTable({ entries, loading, onEdit, onDelete, onClose }: 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>;
return (
<p className="py-8 text-center text-sm text-muted-foreground">
Se încarcă...
</p>
);
}
if (entries.length === 0) {
@@ -62,6 +77,7 @@ export function RegistryTable({ entries, loading, onEdit, onDelete, onClose }: R
<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">Resp.</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>
@@ -69,29 +85,45 @@ export function RegistryTable({ entries, loading, onEdit, onDelete, onClose }: R
</thead>
<tbody>
{entries.map((entry) => {
const overdueDays = (entry.status === 'deschis' || !entry.status) ? getOverdueDays(entry.deadline) : null;
const overdueDays =
entry.status === "deschis" || !entry.status
? 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'
"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 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'}
variant={
entry.direction === "intrat" ? "default" : "secondary"
}
className="text-xs"
>
{DIRECTION_LABELS[entry.direction] ?? entry.direction ?? '—'}
{DIRECTION_LABELS[entry.direction] ??
entry.direction ??
"—"}
</Badge>
</td>
<td className="px-3 py-2 text-xs">{DOC_TYPE_LABELS[entry.documentType] ?? entry.documentType ?? '—'}</td>
<td className="px-3 py-2 text-xs">
{getDocTypeLabel(entry.documentType)}
</td>
<td className="px-3 py-2 max-w-[200px] truncate">
{entry.subject}
{entry.threadParentId && (
<GitBranch className="ml-1 inline h-3 w-3 text-muted-foreground" />
)}
{(entry.linkedEntryIds ?? []).length > 0 && (
<Link2 className="ml-1 inline h-3 w-3 text-muted-foreground" />
)}
@@ -107,17 +139,39 @@ export function RegistryTable({ entries, loading, onEdit, onDelete, onClose }: R
</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 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 max-w-[100px] truncate text-xs">
{entry.assignee ? (
<span className="flex items-center gap-1">
<User className="h-3 w-3 text-muted-foreground shrink-0" />
{entry.assignee}
</span>
) : (
<span className="text-muted-foreground"></span>
)}
</td>
<td className="px-3 py-2 text-xs whitespace-nowrap">
{entry.deadline ? (
<span className={cn(isOverdue && 'font-medium text-destructive')}>
<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>
<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 className="ml-1 text-[10px] text-muted-foreground">
({Math.abs(overdueDays)}z)
</span>
)}
</span>
) : (
@@ -125,21 +179,39 @@ export function RegistryTable({ entries, loading, onEdit, onDelete, onClose }: R
)}
</td>
<td className="px-3 py-2">
<Badge variant={entry.status === 'deschis' ? 'default' : 'outline'}>
<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">
{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)}>
<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)}>
<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>
@@ -155,7 +227,11 @@ export function RegistryTable({ entries, loading, onEdit, onDelete, onClose }: R
function formatDate(iso: string): string {
try {
return new Date(iso).toLocaleDateString('ro-RO', { day: '2-digit', month: '2-digit', year: 'numeric' });
return new Date(iso).toLocaleDateString("ro-RO", {
day: "2-digit",
month: "2-digit",
year: "numeric",
});
} catch {
return iso;
}