"use client"; import { ArrowDownToLine, ArrowUpFromLine, Bell, BellOff, Calendar, CheckCircle2, Clock, Copy, ExternalLink, Eye, FileText, GitBranch, HardDrive, Link2, Paperclip, Pencil, Trash2, User, X, Image as ImageIcon, Reply, Radio, RefreshCw, ChevronDown, ChevronUp, AlertTriangle, ShieldAlert, Timer, } 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 { getDeadlineType } from "../services/deadline-catalog"; 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; /** Resolve a tracked deadline inline */ onResolveDeadline?: (entryId: string, deadlineId: string, resolution: string, note: string) => 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 = { finalizat: "Finalizat", "aprobat-tacit": "Aprobat tacit", respins: "Respins", retras: "Retras", altele: "Altele", }; const DEADLINE_RES_LABELS: Record = { 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, onResolveDeadline, allEntries, }: RegistryEntryDetailProps) { const [previewIndex, setPreviewIndex] = useState(null); const [copiedPath, setCopiedPath] = useState(null); const [monitorConfigOpen, setMonitorConfigOpen] = useState(false); const [monitorEditMode, setMonitorEditMode] = useState(false); // Authority for existing tracking or auto-detected from recipient const trackingAuthority = useMemo(() => { if (!entry) return undefined; if (entry.externalStatusTracking) { return getAuthority(entry.externalStatusTracking.authorityId) ?? undefined; } return undefined; }, [entry]); // Auto-detect if recipient matches a known authority (only when no tracking) const matchedAuthority = useMemo(() => { if (!entry) return undefined; if (entry.externalStatusTracking) 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(); 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 (
{entry.number} {entry.subject}
{/* Action bar */}
{onReply && ( )} {entry.status === "deschis" && ( )}
{/* ── Status row ── */}
{dir.label} {status.label} {getDocTypeLabel(entry.documentType)} {entry.company && ( {entry.company} )}
{/* ── Closure info ── */} {entry.closureInfo && (
Rezoluție: {RESOLUTION_LABELS[entry.closureInfo.resolution] ?? entry.closureInfo.resolution}
{entry.closureInfo.reason && (

Motiv:{" "} {entry.closureInfo.reason}

)} {entry.closureInfo.closedBy && (

Închis de {entry.closureInfo.closedBy} la{" "} {formatDateTime(entry.closureInfo.closedAt)}

)}
)} {/* ── Date ── */}
{entry.registrationDate && entry.registrationDate !== entry.date && ( )} {entry.deadline && ( 0 ? `(${overdueDays} zile depășit)` : overdueDays !== null && overdueDays < 0 ? `(mai sunt ${Math.abs(overdueDays)} zile)` : undefined } /> )} {entry.expiryDate && ( )}
{/* ── Reminders / Alerts summary ── */} {(() => { const alerts: { icon: React.ReactNode; label: string; detail: string; color: string }[] = []; // AC validity reminder if (entry.acValidity?.enabled) { const ac = entry.acValidity; const validityMonths = ac.validityMonths ?? 12; const issuance = new Date(ac.issuanceDate); const validityEnd = new Date(issuance); validityEnd.setMonth(validityEnd.getMonth() + validityMonths); const now = new Date(); now.setHours(0, 0, 0, 0); validityEnd.setHours(0, 0, 0, 0); const daysLeft = Math.ceil((validityEnd.getTime() - now.getTime()) / (1000 * 60 * 60 * 24)); const monthsLeft = Math.ceil(daysLeft / 30); const currentMonth = Math.max(1, validityMonths - monthsLeft + 1); if (ac.reminder.dismissed) { alerts.push({ icon: , label: `AC — Remindere dezactivate`, detail: `Valabilitate ${validityMonths} luni, luna ${currentMonth}/${validityMonths}`, color: "text-muted-foreground", }); } else if (daysLeft <= 30) { alerts.push({ icon: , label: `AC — Expiră în ${daysLeft} zile!`, detail: `Luna ${currentMonth}/${validityMonths} — acțiune imediată`, color: "text-red-600 dark:text-red-400", }); } else if (daysLeft <= 90) { alerts.push({ icon: , label: `AC — Reminder activ (luna ${currentMonth}/${validityMonths})`, detail: `Mai sunt ${daysLeft} zile (${monthsLeft} luni)`, color: "text-amber-600 dark:text-amber-400", }); } else { alerts.push({ icon: , label: `AC — Reminder activ (luna ${currentMonth}/${validityMonths})`, detail: `Mai sunt ${daysLeft} zile`, color: "text-emerald-600 dark:text-emerald-400", }); } if (ac.reminder.snoozeCount > 0) { const lastAlert = alerts[alerts.length - 1]; if (lastAlert) { lastAlert.detail += ` · Amânat de ${ac.reminder.snoozeCount}×`; } } } // Expiry date alert if (entry.expiryDate) { const expiry = new Date(entry.expiryDate); const now = new Date(); now.setHours(0, 0, 0, 0); expiry.setHours(0, 0, 0, 0); const daysLeft = Math.ceil((expiry.getTime() - now.getTime()) / (1000 * 60 * 60 * 24)); const alertDays = entry.expiryAlertDays ?? 30; if (daysLeft < 0) { alerts.push({ icon: , label: `Document expirat de ${Math.abs(daysLeft)} zile`, detail: `Expirat: ${formatDate(entry.expiryDate)}`, color: "text-red-600 dark:text-red-400", }); } else if (daysLeft <= alertDays) { alerts.push({ icon: , label: `Expiră în ${daysLeft} zile`, detail: `Alertă setată la ${alertDays} zile înainte`, color: "text-amber-600 dark:text-amber-400", }); } else { alerts.push({ icon: , label: `Valabil — expiră ${formatDate(entry.expiryDate)}`, detail: `Mai sunt ${daysLeft} zile (alertă la ${alertDays} zile)`, color: "text-emerald-600 dark:text-emerald-400", }); } } // Tracked deadlines summary const pendingDeadlines = (entry.trackedDeadlines ?? []).filter(d => d.resolution === "pending"); if (pendingDeadlines.length > 0) { const overdueCount = pendingDeadlines.filter(d => { const due = new Date(d.dueDate); due.setHours(0, 0, 0, 0); return due.getTime() < new Date().setHours(0, 0, 0, 0); }).length; if (overdueCount > 0) { alerts.push({ icon: , label: `${overdueCount} ${overdueCount === 1 ? "termen depășit" : "termene depășite"}`, detail: `${pendingDeadlines.length} ${pendingDeadlines.length === 1 ? "termen activ" : "termene active"} total`, color: "text-red-600 dark:text-red-400", }); } else { alerts.push({ icon: , label: `${pendingDeadlines.length} ${pendingDeadlines.length === 1 ? "termen activ" : "termene active"}`, detail: pendingDeadlines.map(d => `${getDeadlineType(d.typeId)?.label ?? d.typeId} (${formatDate(d.dueDate)})`).join(", "), color: "text-blue-600 dark:text-blue-400", }); } } if (alerts.length === 0) return null; return (
{alerts.map((alert, i) => (
{alert.icon}

{alert.label}

{alert.detail}

))}
); })()} {/* ── Parties ── */}
{entry.sender && ( )} {entry.recipient && ( )} {entry.assignee && ( } /> )}
{(entry.recipientRegNumber || entry.recipientRegDate) && (
{entry.recipientRegNumber && ( Nr. destinatar: {entry.recipientRegNumber} )} {entry.recipientRegDate && ( Data: {formatDate(entry.recipientRegDate)} )}
)}
{/* ── Thread links (flow diagram) ── */} {(threadChain.length >= 2 || (entry.linkedEntryIds ?? []).length > 0) && ( {threadChain.length >= 2 && ( )} {/* Transmission status for thread children */} {threadChildren.length > 0 && (
{threadChildren.map((child) => { const txStatus = computeTransmissionStatus(entry, child); if (!txStatus) return null; return (
{child.number} {txStatus === "on-time" ? ( Transmis in termen ) : ( Transmis cu intarziere )}
); })}
)} {(entry.linkedEntryIds ?? []).length > 0 && (

{entry.linkedEntryIds.length} inregistrar {entry.linkedEntryIds.length === 1 ? "e" : "i"} legat {entry.linkedEntryIds.length === 1 ? "a" : "e"}

)}
)} {/* ── Attachments ── */} {entry.attachments.length > 0 && (
{entry.attachments.map((att) => att.networkPath ? ( ) : (
{att.type.startsWith("image/") ? ( ) : ( )}

{att.name}

{(att.size / 1024).toFixed(0)} KB •{" "} {att.type.split("/")[1]?.toUpperCase() ?? att.type}

{/* Preview button (images + PDFs) */} {att.data && att.data !== "" && att.data !== "__network__" && (att.type.startsWith("image/") || att.type === "application/pdf") && ( )} {/* Download for files with data */} {att.data && att.data !== "" && att.data !== "__network__" && ( )} Fișier
), )}
)} {/* ── Legal deadlines (timeline view) ── */} {(entry.trackedDeadlines ?? []).length > 0 && ( { onResolveDeadline(entry.id, deadlineId, "completed", note); } : undefined} /> )} {/* ── External status monitoring ── */} {entry.externalStatusTracking && ( <> { setMonitorEditMode(true); setMonitorConfigOpen(true); }} /> {trackingAuthority && ( { setMonitorConfigOpen(open); if (!open) setMonitorEditMode(false); }} entry={entry} authority={trackingAuthority} editMode onActivate={async (tracking) => { 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 } }} /> )} )} {/* ── Auto-detect: suggest monitoring activation ── */} {matchedAuthority && !entry.externalStatusTracking && (

{matchedAuthority.name} suporta monitorizare automata

Se poate verifica automat statusul cererii nr.{" "} {entry.recipientRegNumber} de 4 ori pe zi.

{ 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 } }} />
)} {/* ── External tracking (legacy fields) ── */} {!entry.externalStatusTracking?.active && (entry.externalStatusUrl || entry.externalTrackingId) && (
{entry.externalTrackingId && ( )} {entry.externalStatusUrl && ( )}
)} {/* ── Tags ── */} {entry.tags.length > 0 && (
{entry.tags.map((tag) => ( {tag} ))}
)} {/* ── Notes ── */} {entry.notes && (

{entry.notes}

)}
{/* QuickLook-style attachment preview */} {previewIndex !== null && ( setPreviewIndex(null)} /> )}
); } // ── Sub-components ── // ── External Status Monitoring Section ── const STATUS_COLORS: Record = { "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, onEdit, }: { entry: RegistryEntry; onEdit: () => void; }) { const tracking = entry.externalStatusTracking; if (!tracking) return null; const [checking, setChecking] = useState(false); const [toggling, setToggling] = useState(false); const [checkResult, setCheckResult] = useState<{ changed: boolean; error: string | null; newStatus?: string; } | null>(null); const [showHistory, setShowHistory] = useState(false); const [liveTracking, setLiveTracking] = useState(tracking); const authority = getAuthority(tracking.authorityId); const handleManualCheck = useCallback(async () => { setChecking(true); setCheckResult(null); try { const res = await fetch("/api/registratura/status-check/single", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ entryId: entry.id }), }); const data = (await res.json()) as { changed: boolean; error: string | null; newStatus?: string; tracking?: typeof tracking; }; setCheckResult({ changed: data.changed, error: data.error, newStatus: data.newStatus, }); if (data.tracking) { setLiveTracking(data.tracking); } } catch (err) { setCheckResult({ changed: false, error: err instanceof Error ? err.message : "Eroare conexiune", }); } 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`; }; const handleToggleActive = useCallback(async () => { setToggling(true); try { const updated = { ...liveTracking, active: !liveTracking.active }; await fetch("/api/registratura", { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ id: entry.id, updates: { externalStatusTracking: updated }, }), }); setLiveTracking(updated); } catch { // Best effort } finally { setToggling(false); } }, [entry.id, liveTracking]); const t = liveTracking; return (

Monitorizare status extern {!t.active && ( (oprita) )}

{t.active && ( )}
{/* Inline check result */} {checkResult && (
{checkResult.error ? `Eroare: ${checkResult.error}` : checkResult.changed ? `Status actualizat: ${EXTERNAL_STATUS_LABELS[checkResult.newStatus as ExternalDocStatus] ?? checkResult.newStatus}` : "Nicio schimbare detectata"}
)}
{/* Authority + status badge */}
{authority?.name ?? t.authorityId} {EXTERNAL_STATUS_LABELS[t.semanticStatus]}
{/* Last check time */} {t.lastCheckAt && (

Ultima verificare: {relativeTime(t.lastCheckAt)}

)} {/* Error state */} {t.lastError && (

{t.lastError}

)} {/* Latest status row */} {t.lastStatusRow && (
Sursa:{" "} {t.lastStatusRow.sursa} {" "} {t.lastStatusRow.destinatie}
{t.lastStatusRow.modRezolvare && (
Rezolvare:{" "} {t.lastStatusRow.modRezolvare}
)} {t.lastStatusRow.comentarii && (
{t.lastStatusRow.comentarii}
)}
{t.lastStatusRow.dataVenire} {t.lastStatusRow.oraVenire}
)} {/* Tracking config info */}
Nr: {t.regNumber} | Data: {t.regDate} | Deponent: {t.petitionerName}
{/* History toggle */} {t.history.length > 0 && (
{showHistory && (
{[...t.history].reverse().map((change, i) => (
{EXTERNAL_STATUS_LABELS[change.semanticStatus]} {new Date(change.timestamp).toLocaleString("ro-RO")}
{change.row.sursa} → {change.row.destinatie} {change.row.modRezolvare ? ` (${change.row.modRezolvare})` : ""}
))}
)}
)}
); } function DetailSection({ title, children, }: { title: string; children: React.ReactNode; }) { return (

{title}

{children}
); } function DetailField({ label, value, className, extra, icon, }: { label: string; value: string; className?: string; extra?: string; icon?: React.ReactNode; }) { return (

{label}

{icon && {icon}} {value} {extra && ( {extra} )}

); }