feat: simplify deadline dashboard + add flow diagrams for document chains

Major UX overhaul of the "Termene legale" and thread tabs:

Deadline dashboard:
- Replace 6 KPI cards with simple summary bar (active/urgent/overdue)
- Replace flat table with grouped list by entry (cards with progress bars)
- Chain deadlines collapsed by default with expand toggle
- Auto-tracked/background deadlines hidden from main list

Flow diagram (new component):
- CSS-only horizontal flow diagram showing document chains
- Nodes with direction bar (blue=intrat, orange=iesit), number, subject, status
- Solid arrows for thread links, dashed for conex/linked entries
- Used in both "Dosare" tab (full) and detail panel (compact, max 5 nodes)

Thread explorer → Dosare:
- Renamed tab "Fire conversatie" → "Dosare"
- Each thread shown as a card with flow diagram inside
- Simplified stats (just active/finalized count)

Background tracking:
- comunicare-aviz-beneficiar marked as backgroundOnly (not shown in dashboard)
- Transmission status computed and shown in detail panel (on-time/late)

Auto-resolution:
- When closing entry via reply, matching parent deadlines auto-resolve
- Resolution note includes the reply entry number

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
AI Assistant
2026-03-11 23:25:08 +02:00
parent 34024404a5
commit 5b18cce5a3
8 changed files with 1026 additions and 653 deletions
@@ -49,7 +49,9 @@ import {
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";
interface RegistryEntryDetailProps {
entry: RegistryEntry | null;
@@ -191,6 +193,36 @@ export function RegistryEntryDetail({
(e) => e.threadParentId === entry.id,
);
// Build full chain for mini flow diagram
const threadChain = useMemo(() => {
if (!threadParent && threadChildren.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, threadParent, threadChildren, allEntries]);
return (
<Sheet open={open} onOpenChange={onOpenChange}>
<SheetContent
@@ -387,48 +419,52 @@ export function RegistryEntryDetail({
)}
</DetailSection>
{/* ── Thread links ── */}
{(threadParent ||
threadChildren.length > 0 ||
{/* ── Thread links (flow diagram) ── */}
{(threadChain.length >= 2 ||
(entry.linkedEntryIds ?? []).length > 0) && (
<DetailSection title="Legături">
{threadParent && (
<div className="flex items-center gap-2 text-sm">
<GitBranch className="h-3.5 w-3.5 text-muted-foreground" />
<span className="text-muted-foreground">Răspuns la:</span>
<span className="font-mono text-xs">
{threadParent.number}
</span>
<span className="truncate text-muted-foreground">
{threadParent.subject}
</span>
</div>
<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-1 space-y-0.5">
<p className="text-xs text-muted-foreground">
Răspunsuri ({threadChildren.length}):
</p>
{threadChildren.map((child) => (
<div
key={child.id}
className="flex items-center gap-2 text-xs ml-4"
>
<span className="font-mono">{child.number}</span>
<span className="truncate text-muted-foreground">
{child.subject}
</span>
</div>
))}
<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-1">
<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} înregistrăr
{entry.linkedEntryIds.length} inregistrar
{entry.linkedEntryIds.length === 1 ? "e" : "i"} legat
{entry.linkedEntryIds.length === 1 ? "ă" : "e"}
{entry.linkedEntryIds.length === 1 ? "a" : "e"}
</p>
</div>
)}