Files
ArchiTools/src/modules/registratura/components/registry-entry-detail.tsx
T
AI Assistant d8a10fadc0 fix: React error #310 — useMemo after early return in detail panel
Move threadChain useMemo before the `if (!entry) return null` early
return to keep hook call order stable between renders. When entry was
null, the hook was skipped, causing "Rendered more hooks than during
the previous render" crash on subsequent renders with entry set.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 00:23:15 +02:00

941 lines
34 KiB
TypeScript

"use client";
import {
ArrowDownToLine,
ArrowUpFromLine,
Calendar,
CheckCircle2,
Clock,
Copy,
ExternalLink,
Eye,
FileText,
GitBranch,
HardDrive,
Link2,
Paperclip,
Pencil,
Trash2,
User,
X,
Image as ImageIcon,
Reply,
Radio,
RefreshCw,
ChevronDown,
ChevronUp,
} from "lucide-react";
import { Button } from "@/shared/components/ui/button";
import { Badge } from "@/shared/components/ui/badge";
import { Separator } from "@/shared/components/ui/separator";
import { ScrollArea } from "@/shared/components/ui/scroll-area";
import {
Sheet,
SheetContent,
SheetHeader,
SheetTitle,
SheetDescription,
} from "@/shared/components/ui/sheet";
import type { RegistryEntry } from "../types";
import { DEFAULT_DOC_TYPE_LABELS, EXTERNAL_STATUS_LABELS } from "../types";
import type { ExternalDocStatus } from "../types";
import { getAuthority } from "../services/authority-catalog";
import { getOverdueDays } from "../services/registry-service";
import { pathFileName, shareLabelFor } from "@/config/nas-paths";
import { cn } from "@/shared/lib/utils";
import { useState, useCallback, useMemo } from "react";
import {
AttachmentPreview,
getPreviewableAttachments,
} from "./attachment-preview";
import { findAuthorityForContact } from "../services/authority-catalog";
import { computeTransmissionStatus } from "../services/deadline-service";
import { StatusMonitorConfig } from "./status-monitor-config";
import { FlowDiagram } from "./flow-diagram";
import { DeadlineTimeline } from "./deadline-timeline";
interface RegistryEntryDetailProps {
entry: RegistryEntry | null;
open: boolean;
onOpenChange: (open: boolean) => void;
onEdit: (entry: RegistryEntry) => void;
onClose: (entry: RegistryEntry) => void;
onDelete: (id: string) => void;
/** Create a new entry linked as reply (conex) to this entry */
onReply?: (entry: RegistryEntry) => void;
allEntries: RegistryEntry[];
}
const DIRECTION_CONFIG = {
intrat: {
label: "Intrat",
icon: ArrowDownToLine,
class: "bg-blue-100 text-blue-800 dark:bg-blue-900/40 dark:text-blue-300",
},
iesit: {
label: "Ieșit",
icon: ArrowUpFromLine,
class:
"bg-orange-100 text-orange-800 dark:bg-orange-900/40 dark:text-orange-300",
},
} as const;
const STATUS_CONFIG = {
deschis: {
label: "Deschis",
class:
"bg-emerald-100 text-emerald-800 dark:bg-emerald-900/40 dark:text-emerald-300",
},
inchis: {
label: "Închis",
class: "bg-gray-100 text-gray-600 dark:bg-gray-800 dark:text-gray-400",
},
reserved: {
label: "Rezervat",
class: "bg-amber-100 text-amber-800 dark:bg-amber-900/40 dark:text-amber-300",
},
} as const;
const RESOLUTION_LABELS: Record<string, string> = {
finalizat: "Finalizat",
"aprobat-tacit": "Aprobat tacit",
respins: "Respins",
retras: "Retras",
altele: "Altele",
};
const DEADLINE_RES_LABELS: Record<string, string> = {
pending: "În așteptare",
completed: "Finalizat",
"aprobat-tacit": "Aprobat tacit",
respins: "Respins",
anulat: "Anulat",
};
function getDocTypeLabel(type: string): string {
const label = DEFAULT_DOC_TYPE_LABELS[type];
if (label) return label;
return type.replace(/-/g, " ").replace(/^\w/, (c) => c.toUpperCase());
}
function formatDate(iso: string): string {
try {
return new Date(iso).toLocaleDateString("ro-RO", {
day: "2-digit",
month: "2-digit",
year: "numeric",
});
} catch {
return iso;
}
}
function formatDateTime(iso: string): string {
try {
return new Date(iso).toLocaleString("ro-RO", {
day: "2-digit",
month: "2-digit",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
});
} catch {
return iso;
}
}
export function RegistryEntryDetail({
entry,
open,
onOpenChange,
onEdit,
onClose,
onDelete,
onReply,
allEntries,
}: RegistryEntryDetailProps) {
const [previewIndex, setPreviewIndex] = useState<number | null>(null);
const [copiedPath, setCopiedPath] = useState<string | null>(null);
const [monitorConfigOpen, setMonitorConfigOpen] = useState(false);
// Auto-detect if recipient matches a known authority
const matchedAuthority = useMemo(() => {
if (!entry) return undefined;
if (entry.externalStatusTracking?.active) return undefined;
if (!entry.recipientRegNumber) return undefined;
return findAuthorityForContact(entry.recipient);
}, [entry]);
const previewableAtts = useMemo(
() => (entry ? getPreviewableAttachments(entry.attachments) : []),
[entry],
);
const copyPath = useCallback(async (path: string) => {
await navigator.clipboard.writeText(path);
setCopiedPath(path);
setTimeout(() => setCopiedPath(null), 2000);
}, []);
// Build full chain for mini flow diagram (must be before early return to keep hook order stable)
const threadChain = useMemo(() => {
if (!entry) return [];
const threadParentEntry = entry.threadParentId
? allEntries.find((e) => e.id === entry.threadParentId)
: null;
const threadChildEntries = allEntries.filter(
(e) => e.threadParentId === entry.id,
);
if (!threadParentEntry && threadChildEntries.length === 0) return [];
const byId = new Map(allEntries.map((e) => [e.id, e]));
// Walk up to root
let root = entry;
while (root.threadParentId) {
const parent = byId.get(root.threadParentId);
if (!parent) break;
root = parent;
}
// BFS down from root
const chain: RegistryEntry[] = [];
const queue = [root.id];
const visited = new Set<string>();
while (queue.length > 0) {
const id = queue.shift()!;
if (visited.has(id)) continue;
visited.add(id);
const e = byId.get(id);
if (!e) continue;
chain.push(e);
for (const child of allEntries.filter((c) => c.threadParentId === id)) {
queue.push(child.id);
}
}
chain.sort((a, b) => a.date.localeCompare(b.date));
return chain;
}, [entry, allEntries]);
if (!entry) return null;
const dir = DIRECTION_CONFIG[entry.direction] ?? DIRECTION_CONFIG.intrat;
const DirIcon = dir.icon;
const status = STATUS_CONFIG[entry.status] ?? STATUS_CONFIG.deschis;
const overdueDays =
entry.status === "deschis" ? getOverdueDays(entry.deadline) : null;
const isOverdue = overdueDays !== null && overdueDays > 0;
const threadParent = entry.threadParentId
? allEntries.find((e) => e.id === entry.threadParentId)
: null;
const threadChildren = allEntries.filter(
(e) => e.threadParentId === entry.id,
);
return (
<Sheet open={open} onOpenChange={onOpenChange}>
<SheetContent
side="right"
className="w-full sm:max-w-lg md:max-w-xl p-0 flex flex-col"
>
<SheetHeader className="px-6 pt-6 pb-0">
<div className="flex items-start justify-between gap-3 pr-8">
<div className="space-y-1">
<SheetTitle className="text-lg font-bold font-mono">
{entry.number}
</SheetTitle>
<SheetDescription className="text-sm">
{entry.subject}
</SheetDescription>
</div>
</div>
{/* Action bar */}
<div className="flex gap-2 mt-3">
<Button
size="sm"
variant="outline"
onClick={() => {
onOpenChange(false);
onEdit(entry);
}}
>
<Pencil className="mr-1.5 h-3.5 w-3.5" /> Editează
</Button>
{onReply && (
<Button
size="sm"
variant="outline"
className="text-blue-600 border-blue-300 hover:bg-blue-50 dark:border-blue-700 dark:hover:bg-blue-950/30"
onClick={() => {
onOpenChange(false);
onReply(entry);
}}
>
<Reply className="mr-1.5 h-3.5 w-3.5" /> Conex
</Button>
)}
{entry.status === "deschis" && (
<Button
size="sm"
variant="outline"
className="text-green-600 border-green-300 hover:bg-green-50 dark:border-green-700 dark:hover:bg-green-950/30"
onClick={() => {
onOpenChange(false);
onClose(entry);
}}
>
<CheckCircle2 className="mr-1.5 h-3.5 w-3.5" /> Inchide
</Button>
)}
<Button
size="sm"
variant="ghost"
className="text-destructive ml-auto"
onClick={() => {
onOpenChange(false);
onDelete(entry.id);
}}
>
<Trash2 className="mr-1.5 h-3.5 w-3.5" /> Șterge
</Button>
</div>
</SheetHeader>
<Separator className="mt-4" />
<ScrollArea className="flex-1 min-h-0 overflow-hidden [&>[data-slot=scroll-area-viewport]]:!overflow-x-hidden">
<div className="space-y-5 py-4 px-6">
{/* ── Status row ── */}
<div className="flex flex-wrap gap-2">
<Badge className={cn("text-xs px-2.5 py-0.5", dir.class)}>
<DirIcon className="mr-1 h-3 w-3" />
{dir.label}
</Badge>
<Badge className={cn("text-xs px-2.5 py-0.5", status.class)}>
{status.label}
</Badge>
<Badge variant="outline" className="text-xs">
{getDocTypeLabel(entry.documentType)}
</Badge>
{entry.company && (
<Badge variant="outline" className="text-xs uppercase">
{entry.company}
</Badge>
)}
</div>
{/* ── Closure info ── */}
{entry.closureInfo && (
<DetailSection title="Închidere">
<div className="rounded-md border border-muted p-3 space-y-1 text-sm">
<div className="flex items-center gap-2">
<span className="text-muted-foreground">Rezoluție:</span>
<Badge variant="outline" className="text-xs">
{RESOLUTION_LABELS[entry.closureInfo.resolution] ??
entry.closureInfo.resolution}
</Badge>
</div>
{entry.closureInfo.reason && (
<p>
<span className="text-muted-foreground">Motiv:</span>{" "}
{entry.closureInfo.reason}
</p>
)}
{entry.closureInfo.closedBy && (
<p className="text-xs text-muted-foreground">
Închis de {entry.closureInfo.closedBy} la{" "}
{formatDateTime(entry.closureInfo.closedAt)}
</p>
)}
</div>
</DetailSection>
)}
{/* ── Date ── */}
<DetailSection title="Date">
<div className="grid grid-cols-2 gap-3">
<DetailField
label="Data document"
value={formatDate(entry.date)}
/>
{entry.registrationDate &&
entry.registrationDate !== entry.date && (
<DetailField
label="Data înregistrare"
value={formatDate(entry.registrationDate)}
/>
)}
{entry.deadline && (
<DetailField
label="Termen limită"
value={formatDate(entry.deadline)}
className={cn(isOverdue && "text-destructive font-medium")}
extra={
overdueDays !== null && overdueDays > 0
? `(${overdueDays} zile depășit)`
: overdueDays !== null && overdueDays < 0
? `(mai sunt ${Math.abs(overdueDays)} zile)`
: undefined
}
/>
)}
{entry.expiryDate && (
<DetailField
label="Valabilitate"
value={formatDate(entry.expiryDate)}
/>
)}
<DetailField
label="Creat"
value={formatDateTime(entry.createdAt)}
/>
<DetailField
label="Modificat"
value={formatDateTime(entry.updatedAt)}
/>
</div>
</DetailSection>
{/* ── Parties ── */}
<DetailSection title="Părți">
<div className="grid grid-cols-2 gap-3">
{entry.sender && (
<DetailField label="Expeditor" value={entry.sender} />
)}
{entry.recipient && (
<DetailField label="Destinatar" value={entry.recipient} />
)}
{entry.assignee && (
<DetailField
label="Responsabil"
value={entry.assignee}
icon={<User className="h-3 w-3 text-muted-foreground" />}
/>
)}
</div>
{(entry.recipientRegNumber || entry.recipientRegDate) && (
<div className="mt-2 rounded border border-dashed p-2 text-xs text-muted-foreground">
{entry.recipientRegNumber && (
<span>Nr. destinatar: {entry.recipientRegNumber}</span>
)}
{entry.recipientRegDate && (
<span className="ml-3">
Data: {formatDate(entry.recipientRegDate)}
</span>
)}
</div>
)}
</DetailSection>
{/* ── Thread links (flow diagram) ── */}
{(threadChain.length >= 2 ||
(entry.linkedEntryIds ?? []).length > 0) && (
<DetailSection title="Dosar">
{threadChain.length >= 2 && (
<FlowDiagram
chain={threadChain}
allEntries={allEntries}
highlightEntryId={entry.id}
compact
maxNodes={5}
/>
)}
{/* Transmission status for thread children */}
{threadChildren.length > 0 && (
<div className="mt-2 space-y-1">
{threadChildren.map((child) => {
const txStatus = computeTransmissionStatus(entry, child);
if (!txStatus) return null;
return (
<div
key={child.id}
className="flex items-center gap-2 text-[10px]"
>
<span className="font-mono">{child.number}</span>
{txStatus === "on-time" ? (
<span className="text-green-600 dark:text-green-400">
Transmis in termen
</span>
) : (
<span className="text-amber-600 dark:text-amber-400">
Transmis cu intarziere
</span>
)}
</div>
);
})}
</div>
)}
{(entry.linkedEntryIds ?? []).length > 0 && (
<div className="mt-2">
<p className="text-xs text-muted-foreground flex items-center gap-1">
<Link2 className="h-3 w-3" />
{entry.linkedEntryIds.length} inregistrar
{entry.linkedEntryIds.length === 1 ? "e" : "i"} legat
{entry.linkedEntryIds.length === 1 ? "a" : "e"}
</p>
</div>
)}
</DetailSection>
)}
{/* ── Attachments ── */}
{entry.attachments.length > 0 && (
<DetailSection title={`Atașamente (${entry.attachments.length})`}>
<div className="space-y-2">
{entry.attachments.map((att) =>
att.networkPath ? (
<button
type="button"
key={att.id}
className="w-full text-left rounded-md border border-blue-200 dark:border-blue-800 bg-blue-50/50 dark:bg-blue-950/20 px-3 py-2 group cursor-pointer hover:bg-blue-100/50 dark:hover:bg-blue-900/30 transition-colors"
onClick={() => copyPath(att.networkPath!)}
title={att.networkPath}
>
<div className="flex items-center gap-2">
<HardDrive className="h-4 w-4 text-blue-600 dark:text-blue-400 shrink-0" />
<span className="text-sm text-blue-700 dark:text-blue-300 truncate block">
{pathFileName(att.networkPath)}
</span>
<span className="ml-auto flex items-center gap-1.5 shrink-0">
<Badge
variant="outline"
className="text-[10px] border-blue-300 dark:border-blue-700 text-blue-600 dark:text-blue-400"
>
{shareLabelFor(att.networkPath) ?? "NAS"}
</Badge>
{copiedPath === att.networkPath ? (
<Badge className="text-[10px] bg-green-600 text-white animate-in fade-in">
Copiat!
</Badge>
) : (
<Badge
variant="outline"
className="text-[10px] border-blue-300 dark:border-blue-700 text-blue-600 dark:text-blue-400 opacity-60 group-hover:opacity-100"
>
<Copy className="mr-1 h-3 w-3" />
Copiază
</Badge>
)}
</span>
</div>
</button>
) : (
<div
key={att.id}
className="flex items-center gap-2 rounded-md border px-3 py-2 group"
>
{att.type.startsWith("image/") ? (
<ImageIcon className="h-4 w-4 text-muted-foreground shrink-0" />
) : (
<FileText className="h-4 w-4 text-muted-foreground shrink-0" />
)}
<div className="flex-1 min-w-0">
<p className="text-sm truncate">{att.name}</p>
<p className="text-[10px] text-muted-foreground">
{(att.size / 1024).toFixed(0)} KB {" "}
{att.type.split("/")[1]?.toUpperCase() ?? att.type}
</p>
</div>
<div className="flex items-center gap-1 shrink-0">
{/* Preview button (images + PDFs) */}
{att.data &&
att.data !== "" &&
att.data !== "__network__" &&
(att.type.startsWith("image/") ||
att.type === "application/pdf") && (
<Button
type="button"
variant="ghost"
size="icon"
className="h-6 w-6"
onClick={() => {
const idx = previewableAtts.findIndex(
(a) => a.id === att.id,
);
if (idx >= 0) setPreviewIndex(idx);
}}
title="Previzualizare"
>
<Eye className="h-3 w-3" />
</Button>
)}
{/* Download for files with data */}
{att.data &&
att.data !== "" &&
att.data !== "__network__" && (
<Button
type="button"
variant="ghost"
size="icon"
className="h-6 w-6"
onClick={() => {
const a = document.createElement("a");
a.href = att.data;
a.download = att.name;
a.click();
}}
title="Descarcă"
>
<ExternalLink className="h-3 w-3" />
</Button>
)}
<Badge variant="outline" className="text-[10px]">
<Paperclip className="mr-0.5 h-2.5 w-2.5" />
Fișier
</Badge>
</div>
</div>
),
)}
</div>
</DetailSection>
)}
{/* ── Legal deadlines (timeline view) ── */}
{(entry.trackedDeadlines ?? []).length > 0 && (
<DetailSection title="Termene legale">
<DeadlineTimeline deadlines={entry.trackedDeadlines!} />
</DetailSection>
)}
{/* ── External status monitoring ── */}
{entry.externalStatusTracking?.active && (
<ExternalStatusSection
entry={entry}
/>
)}
{/* ── Auto-detect: suggest monitoring activation ── */}
{matchedAuthority && !entry.externalStatusTracking?.active && (
<div className="rounded-lg border border-dashed border-blue-300 dark:border-blue-700 bg-blue-50/50 dark:bg-blue-950/20 p-3">
<div className="flex items-start gap-2">
<Radio className="h-4 w-4 text-blue-500 mt-0.5 shrink-0" />
<div className="flex-1">
<p className="text-xs font-medium">
{matchedAuthority.name} suporta monitorizare automata
</p>
<p className="text-[10px] text-muted-foreground mt-0.5">
Se poate verifica automat statusul cererii nr.{" "}
{entry.recipientRegNumber} de 4 ori pe zi.
</p>
<Button
variant="outline"
size="sm"
className="mt-2 h-6 text-xs"
onClick={() => setMonitorConfigOpen(true)}
>
Configureaza monitorizarea
</Button>
</div>
</div>
<StatusMonitorConfig
open={monitorConfigOpen}
onOpenChange={setMonitorConfigOpen}
entry={entry}
authority={matchedAuthority}
onActivate={async (tracking) => {
// Save tracking to entry via API
try {
await fetch("/api/registratura", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
id: entry.id,
updates: { externalStatusTracking: tracking },
}),
});
window.location.reload();
} catch {
// Best effort
}
}}
/>
</div>
)}
{/* ── External tracking (legacy fields) ── */}
{!entry.externalStatusTracking?.active &&
(entry.externalStatusUrl || entry.externalTrackingId) && (
<DetailSection title="Urmărire externă">
<div className="grid grid-cols-2 gap-3">
{entry.externalTrackingId && (
<DetailField
label="ID extern"
value={entry.externalTrackingId}
/>
)}
{entry.externalStatusUrl && (
<div>
<p className="text-[10px] text-muted-foreground mb-0.5">
URL verificare
</p>
<a
href={entry.externalStatusUrl}
target="_blank"
rel="noopener noreferrer"
className="text-xs text-blue-600 dark:text-blue-400 hover:underline break-all"
>
{entry.externalStatusUrl}
</a>
</div>
)}
</div>
</DetailSection>
)}
{/* ── Tags ── */}
{entry.tags.length > 0 && (
<DetailSection title="Etichete">
<div className="flex flex-wrap gap-1">
{entry.tags.map((tag) => (
<Badge key={tag} variant="outline" className="text-xs">
{tag}
</Badge>
))}
</div>
</DetailSection>
)}
{/* ── Notes ── */}
{entry.notes && (
<DetailSection title="Note">
<p className="text-sm whitespace-pre-wrap text-muted-foreground">
{entry.notes}
</p>
</DetailSection>
)}
</div>
</ScrollArea>
{/* QuickLook-style attachment preview */}
{previewIndex !== null && (
<AttachmentPreview
key={previewIndex}
attachments={previewableAtts}
initialIndex={previewIndex}
open
onClose={() => setPreviewIndex(null)}
/>
)}
</SheetContent>
</Sheet>
);
}
// ── Sub-components ──
// ── External Status Monitoring Section ──
const STATUS_COLORS: Record<ExternalDocStatus, string> = {
"in-operare": "bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-400",
trimis: "bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400",
solutionat: "bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400",
respins: "bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400",
necunoscut: "bg-muted text-muted-foreground",
};
function ExternalStatusSection({ entry }: { entry: RegistryEntry }) {
const tracking = entry.externalStatusTracking;
if (!tracking) return null;
const [checking, setChecking] = useState(false);
const [showHistory, setShowHistory] = useState(false);
const authority = getAuthority(tracking.authorityId);
const handleManualCheck = useCallback(async () => {
setChecking(true);
try {
await fetch("/api/registratura/status-check/single", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ entryId: entry.id }),
});
// Reload page to show updated status
window.location.reload();
} catch {
// Ignore — user will see if it worked on reload
} finally {
setChecking(false);
}
}, [entry.id]);
const relativeTime = (iso: string) => {
const diff = Date.now() - new Date(iso).getTime();
const mins = Math.floor(diff / 60000);
if (mins < 1) return "chiar acum";
if (mins < 60) return `acum ${mins} min`;
const hrs = Math.floor(mins / 60);
if (hrs < 24) return `acum ${hrs}h`;
const days = Math.floor(hrs / 24);
return `acum ${days}z`;
};
return (
<div>
<div className="flex items-center justify-between mb-2">
<h3 className="text-xs font-semibold text-muted-foreground uppercase tracking-wider">
Monitorizare status extern
</h3>
<Button
variant="ghost"
size="sm"
className="h-6 px-2 text-xs"
onClick={handleManualCheck}
disabled={checking}
>
<RefreshCw className={cn("h-3 w-3 mr-1", checking && "animate-spin")} />
{checking ? "Se verifică..." : "Verifică acum"}
</Button>
</div>
<div className="space-y-2">
{/* Authority + status badge */}
<div className="flex items-center gap-2 flex-wrap">
<span className="text-xs text-muted-foreground">
{authority?.name ?? tracking.authorityId}
</span>
<Badge className={cn("text-[10px] px-1.5 py-0", STATUS_COLORS[tracking.semanticStatus])}>
<Radio className="mr-0.5 inline h-2.5 w-2.5" />
{EXTERNAL_STATUS_LABELS[tracking.semanticStatus]}
</Badge>
</div>
{/* Last check time */}
{tracking.lastCheckAt && (
<p className="text-[10px] text-muted-foreground">
Ultima verificare: {relativeTime(tracking.lastCheckAt)}
</p>
)}
{/* Error state */}
{tracking.lastError && (
<p className="text-[10px] text-red-500">{tracking.lastError}</p>
)}
{/* Latest status row */}
{tracking.lastStatusRow && (
<div className="rounded border bg-muted/30 p-2 text-xs space-y-1">
<div className="flex gap-3">
<span>
<span className="text-muted-foreground">Sursa:</span>{" "}
{tracking.lastStatusRow.sursa}
</span>
<span>
<span className="text-muted-foreground"></span>{" "}
{tracking.lastStatusRow.destinatie}
</span>
</div>
{tracking.lastStatusRow.modRezolvare && (
<div>
<span className="text-muted-foreground">Rezolvare:</span>{" "}
<span className="font-medium">{tracking.lastStatusRow.modRezolvare}</span>
</div>
)}
{tracking.lastStatusRow.comentarii && (
<div className="text-muted-foreground">
{tracking.lastStatusRow.comentarii}
</div>
)}
<div className="text-muted-foreground">
{tracking.lastStatusRow.dataVenire} {tracking.lastStatusRow.oraVenire}
</div>
</div>
)}
{/* History toggle */}
{tracking.history.length > 0 && (
<div>
<button
className="flex items-center gap-1 text-[10px] text-muted-foreground hover:text-foreground transition-colors"
onClick={() => setShowHistory(!showHistory)}
>
{showHistory ? (
<ChevronUp className="h-3 w-3" />
) : (
<ChevronDown className="h-3 w-3" />
)}
Istoric ({tracking.history.length} schimbări)
</button>
{showHistory && (
<div className="mt-1 space-y-1.5 max-h-48 overflow-y-auto">
{[...tracking.history].reverse().map((change, i) => (
<div
key={`${change.timestamp}-${i}`}
className="rounded border bg-muted/20 p-1.5 text-[10px]"
>
<div className="flex items-center gap-2">
<Badge
className={cn(
"text-[8px] px-1 py-0",
STATUS_COLORS[change.semanticStatus],
)}
>
{EXTERNAL_STATUS_LABELS[change.semanticStatus]}
</Badge>
<span className="text-muted-foreground">
{new Date(change.timestamp).toLocaleString("ro-RO")}
</span>
</div>
<div className="mt-0.5 text-muted-foreground">
{change.row.sursa} {change.row.destinatie}
{change.row.modRezolvare ? ` (${change.row.modRezolvare})` : ""}
</div>
</div>
))}
</div>
)}
</div>
)}
</div>
</div>
);
}
function DetailSection({
title,
children,
}: {
title: string;
children: React.ReactNode;
}) {
return (
<div>
<h3 className="text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-2">
{title}
</h3>
{children}
</div>
);
}
function DetailField({
label,
value,
className,
extra,
icon,
}: {
label: string;
value: string;
className?: string;
extra?: string;
icon?: React.ReactNode;
}) {
return (
<div>
<p className="text-[10px] text-muted-foreground mb-0.5">{label}</p>
<p className={cn("text-sm", className)}>
{icon && <span className="mr-1 inline-flex align-middle">{icon}</span>}
{value}
{extra && (
<span className="ml-1 text-[10px] text-muted-foreground">
{extra}
</span>
)}
</p>
</div>
);
}