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:
@@ -94,6 +94,7 @@ export const DEADLINE_CATALOG: DeadlineTypeDef[] = [
|
||||
category: "certificat",
|
||||
legalReference: "Legea 350/2001, art. 44 alin. (4)",
|
||||
autoTrack: true,
|
||||
backgroundOnly: true,
|
||||
directionFilter: ["iesit"],
|
||||
},
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import type {
|
||||
DeadlineAuditEntry,
|
||||
} from "../types";
|
||||
import { getDeadlineType } from "./deadline-catalog";
|
||||
import { computeDueDate } from "./working-days";
|
||||
import { computeDueDate, addWorkingDays } from "./working-days";
|
||||
|
||||
export interface DeadlineDisplayStatus {
|
||||
label: string;
|
||||
@@ -219,3 +219,163 @@ function formatDate(d: Date): string {
|
||||
const day = String(d.getDate()).padStart(2, "0");
|
||||
return `${y}-${m}-${day}`;
|
||||
}
|
||||
|
||||
// ── Grouped deadlines for simplified dashboard ──
|
||||
|
||||
export interface DeadlineEntryGroup {
|
||||
entry: RegistryEntry;
|
||||
/** Main deadlines (user-created, non-background) */
|
||||
mainDeadlines: Array<{
|
||||
deadline: TrackedDeadline;
|
||||
status: DeadlineDisplayStatus;
|
||||
chainChildren: Array<{
|
||||
deadline: TrackedDeadline;
|
||||
status: DeadlineDisplayStatus;
|
||||
}>;
|
||||
}>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Group deadlines by entry for the simplified dashboard.
|
||||
* Only includes entries with pending main deadlines.
|
||||
* Auto-tracked / background deadlines are nested under their chain parent.
|
||||
*/
|
||||
export function groupDeadlinesByEntry(
|
||||
entries: RegistryEntry[],
|
||||
): DeadlineEntryGroup[] {
|
||||
const groups: DeadlineEntryGroup[] = [];
|
||||
|
||||
for (const entry of entries) {
|
||||
const deadlines = entry.trackedDeadlines ?? [];
|
||||
if (deadlines.length === 0) continue;
|
||||
|
||||
// Separate: main deadlines vs chain children vs background
|
||||
const chainChildMap = new Map<
|
||||
string,
|
||||
Array<{ deadline: TrackedDeadline; status: DeadlineDisplayStatus }>
|
||||
>();
|
||||
const mainItems: Array<{
|
||||
deadline: TrackedDeadline;
|
||||
status: DeadlineDisplayStatus;
|
||||
chainChildren: Array<{
|
||||
deadline: TrackedDeadline;
|
||||
status: DeadlineDisplayStatus;
|
||||
}>;
|
||||
}> = [];
|
||||
|
||||
// First pass: identify chain children
|
||||
for (const dl of deadlines) {
|
||||
const def = getDeadlineType(dl.typeId);
|
||||
if (def?.backgroundOnly) continue; // skip background-only
|
||||
|
||||
if (dl.chainParentId) {
|
||||
const children = chainChildMap.get(dl.chainParentId) ?? [];
|
||||
children.push({ deadline: dl, status: getDeadlineDisplayStatus(dl) });
|
||||
chainChildMap.set(dl.chainParentId, children);
|
||||
}
|
||||
}
|
||||
|
||||
// Second pass: build main deadline items
|
||||
for (const dl of deadlines) {
|
||||
const def = getDeadlineType(dl.typeId);
|
||||
if (def?.backgroundOnly) continue;
|
||||
if (dl.chainParentId) continue; // chain children are nested
|
||||
|
||||
// Auto-tracked without chain parent: treat as main if pending, skip if resolved
|
||||
if (def?.autoTrack && dl.resolution !== "pending") continue;
|
||||
|
||||
mainItems.push({
|
||||
deadline: dl,
|
||||
status: getDeadlineDisplayStatus(dl),
|
||||
chainChildren: chainChildMap.get(dl.id) ?? [],
|
||||
});
|
||||
}
|
||||
|
||||
// Only include entries that have pending main deadlines
|
||||
const hasPending = mainItems.some((m) => m.deadline.resolution === "pending");
|
||||
if (!hasPending) continue;
|
||||
|
||||
groups.push({ entry, mainDeadlines: mainItems });
|
||||
}
|
||||
|
||||
// Sort: entries with overdue/urgent deadlines first
|
||||
groups.sort((a, b) => {
|
||||
const worstA = worstVariant(a.mainDeadlines.map((m) => m.status.variant));
|
||||
const worstB = worstVariant(b.mainDeadlines.map((m) => m.status.variant));
|
||||
return worstA - worstB;
|
||||
});
|
||||
|
||||
return groups;
|
||||
}
|
||||
|
||||
function worstVariant(variants: string[]): number {
|
||||
const order: Record<string, number> = {
|
||||
red: 0,
|
||||
yellow: 1,
|
||||
blue: 2,
|
||||
green: 3,
|
||||
gray: 4,
|
||||
};
|
||||
let worst = 4;
|
||||
for (const v of variants) {
|
||||
const o = order[v] ?? 4;
|
||||
if (o < worst) worst = o;
|
||||
}
|
||||
return worst;
|
||||
}
|
||||
|
||||
// ── Transmission status (background check) ──
|
||||
|
||||
export type TransmissionStatus = "on-time" | "late" | null;
|
||||
|
||||
/**
|
||||
* Compute whether a reply entry was transmitted within the legal 1-day window.
|
||||
* Returns null if recipient registration date is not available.
|
||||
*/
|
||||
export function computeTransmissionStatus(
|
||||
parentEntry: RegistryEntry,
|
||||
replyEntry: RegistryEntry,
|
||||
): TransmissionStatus {
|
||||
if (!parentEntry.recipientRegDate) return null;
|
||||
|
||||
const regDate = new Date(parentEntry.recipientRegDate);
|
||||
regDate.setHours(0, 0, 0, 0);
|
||||
|
||||
// 1 working day after recipient registration date
|
||||
const deadline = addWorkingDays(regDate, 1);
|
||||
|
||||
const replyDate = new Date(replyEntry.date);
|
||||
replyDate.setHours(0, 0, 0, 0);
|
||||
|
||||
return replyDate.getTime() <= deadline.getTime() ? "on-time" : "late";
|
||||
}
|
||||
|
||||
// ── Auto-resolution of deadlines ──
|
||||
|
||||
/**
|
||||
* Find deadlines on a parent entry that should be auto-resolved when a reply arrives.
|
||||
* Only returns pending deadlines that match the reply context.
|
||||
*/
|
||||
export function findAutoResolvableDeadlines(
|
||||
parentEntry: RegistryEntry,
|
||||
replyEntry: RegistryEntry,
|
||||
): TrackedDeadline[] {
|
||||
const pending = (parentEntry.trackedDeadlines ?? []).filter(
|
||||
(dl) => dl.resolution === "pending",
|
||||
);
|
||||
if (pending.length === 0) return [];
|
||||
|
||||
// Only auto-resolve when reply is incoming (response to our outgoing request)
|
||||
if (replyEntry.direction !== "intrat") return [];
|
||||
if (parentEntry.direction !== "iesit") return [];
|
||||
|
||||
// Filter: skip background-only and auto-tracked chain children
|
||||
return pending.filter((dl) => {
|
||||
const def = getDeadlineType(dl.typeId);
|
||||
if (!def) return false;
|
||||
if (def.backgroundOnly) return false;
|
||||
// Include main deadlines (no chain parent) — these are the ones the user cares about
|
||||
if (!dl.chainParentId) return true;
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user