import { v4 as uuid } from "uuid"; import type { TrackedDeadline, DeadlineResolution, RegistryEntry, DeadlineAuditEntry, } from "../types"; import { getDeadlineType } from "./deadline-catalog"; import { computeDueDate } from "./working-days"; export interface DeadlineDisplayStatus { label: string; variant: "green" | "yellow" | "red" | "blue" | "gray"; daysRemaining: number | null; } /** * Create a new tracked deadline from a type definition + start date. */ export function createTrackedDeadline( typeId: string, startDate: string, chainParentId?: string, ): TrackedDeadline | null { const def = getDeadlineType(typeId); if (!def) return null; const start = new Date(startDate); start.setHours(0, 0, 0, 0); const due = computeDueDate( start, def.days, def.dayType, def.isBackwardDeadline, ); return { id: uuid(), typeId, startDate, dueDate: formatDate(due), resolution: "pending", chainParentId, auditLog: [ { action: "created", timestamp: new Date().toISOString(), detail: `Termen creat: ${def.label} (${def.days} ${def.dayType === "working" ? "zile lucrătoare" : "zile calendaristice"})`, }, ], createdAt: new Date().toISOString(), }; } /** * Resolve a deadline with a given resolution. */ export function resolveDeadline( deadline: TrackedDeadline, resolution: DeadlineResolution, note?: string, ): TrackedDeadline { const auditEntry: DeadlineAuditEntry = { action: "resolved", timestamp: new Date().toISOString(), detail: `Rezolvat: ${resolution}${note ? ` — ${note}` : ""}`, }; return { ...deadline, resolution, resolvedDate: new Date().toISOString(), resolutionNote: note, auditLog: [...(deadline.auditLog ?? []), auditEntry], }; } /** * Get the display status for a tracked deadline — color coding + label. */ export function getDeadlineDisplayStatus( deadline: TrackedDeadline, ): DeadlineDisplayStatus { const def = getDeadlineType(deadline.typeId); // Already resolved if (deadline.resolution !== "pending") { if (deadline.resolution === "aprobat-tacit") { return { label: "Aprobat tacit", variant: "blue", daysRemaining: null }; } if (deadline.resolution === "respins") { return { label: "Respins", variant: "gray", daysRemaining: null }; } if (deadline.resolution === "anulat") { return { label: "Anulat", variant: "gray", daysRemaining: null }; } return { label: "Finalizat", variant: "gray", daysRemaining: null }; } // Pending — compute days remaining const now = new Date(); now.setHours(0, 0, 0, 0); const due = new Date(deadline.dueDate); due.setHours(0, 0, 0, 0); const diff = due.getTime() - now.getTime(); const daysRemaining = Math.ceil(diff / (1000 * 60 * 60 * 24)); // Overdue + tacit applicable → tacit approval if (daysRemaining < 0 && def?.tacitApprovalApplicable) { return { label: "Aprobat tacit", variant: "blue", daysRemaining }; } if (daysRemaining < 0) { return { label: "Depășit termen", variant: "red", daysRemaining }; } if (daysRemaining <= 5) { return { label: "Urgent", variant: "yellow", daysRemaining }; } return { label: "În termen", variant: "green", daysRemaining }; } /** * Aggregate deadline stats across all entries. */ export function aggregateDeadlines(entries: RegistryEntry[]): { active: number; urgent: number; overdue: number; tacit: number; missingRecipientReg: number; expiringSoon: number; all: Array<{ deadline: TrackedDeadline; entry: RegistryEntry; status: DeadlineDisplayStatus; }>; } { let active = 0; let urgent = 0; let overdue = 0; let tacit = 0; const all: Array<{ deadline: TrackedDeadline; entry: RegistryEntry; status: DeadlineDisplayStatus; }> = []; // Count entries missing recipient registration (outgoing with deadlines) let missingRecipientReg = 0; let expiringSoon = 0; const now = new Date(); now.setHours(0, 0, 0, 0); for (const entry of entries) { // Check missing recipient registration for outgoing entries if ( entry.direction === "iesit" && entry.status === "deschis" && (entry.trackedDeadlines ?? []).length > 0 && !entry.recipientRegDate ) { missingRecipientReg++; } // Check document expiry if (entry.expiryDate && entry.status === "deschis") { const expiry = new Date(entry.expiryDate); 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 <= alertDays) { expiringSoon++; } } for (const dl of entry.trackedDeadlines ?? []) { const status = getDeadlineDisplayStatus(dl); all.push({ deadline: dl, entry, status }); if (dl.resolution === "pending") { active++; if (status.variant === "yellow") urgent++; if (status.variant === "red") overdue++; if (status.variant === "blue") tacit++; } else if (dl.resolution === "aprobat-tacit") { tacit++; } } } // Sort: overdue first, then by due date ascending all.sort((a, b) => { const aP = a.deadline.resolution === "pending" ? 0 : 1; const bP = b.deadline.resolution === "pending" ? 0 : 1; if (aP !== bP) return aP - bP; return a.deadline.dueDate.localeCompare(b.deadline.dueDate); }); return { active, urgent, overdue, tacit, missingRecipientReg, expiringSoon, all, }; } function formatDate(d: Date): string { const y = d.getFullYear(); const m = String(d.getMonth() + 1).padStart(2, "0"); const day = String(d.getDate()).padStart(2, "0"); return `${y}-${m}-${day}`; }