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