3.03 Registratura Termene Legale recipient registration, audit log, expiry tracking

- Added recipientRegNumber/recipientRegDate fields for outgoing docs (deadline triggers from recipient registration date)
- Added prelungire-CU deadline type in catalog (15 calendar days, tacit approval)
- CU category already first in catalog  verified
- DeadlineAuditEntry interface + audit log on TrackedDeadline (created/resolved entries)
- Document expiry tracking: expiryDate + expiryAlertDays with live countdown
- Web scraping prep fields: externalStatusUrl + externalTrackingId
- Dashboard: 6 stat cards (added missing recipient + expiring soon)
- Alert banners for missing recipient data and expiring documents
- Version bump to 0.2.0
This commit is contained in:
AI Assistant
2026-02-28 04:31:32 +02:00
parent 85bdb59da4
commit 99fbdddb68
9 changed files with 649 additions and 150 deletions
@@ -1,11 +1,16 @@
import { v4 as uuid } from 'uuid';
import type { TrackedDeadline, DeadlineResolution, RegistryEntry } from '../types';
import { getDeadlineType } from './deadline-catalog';
import { computeDueDate } from './working-days';
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';
variant: "green" | "yellow" | "red" | "blue" | "gray";
daysRemaining: number | null;
}
@@ -23,15 +28,27 @@ export function createTrackedDeadline(
const start = new Date(startDate);
start.setHours(0, 0, 0, 0);
const due = computeDueDate(start, def.days, def.dayType, def.isBackwardDeadline);
const due = computeDueDate(
start,
def.days,
def.dayType,
def.isBackwardDeadline,
);
return {
id: uuid(),
typeId,
startDate,
dueDate: formatDate(due),
resolution: 'pending',
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(),
};
}
@@ -44,32 +61,40 @@ export function resolveDeadline(
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 {
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 !== "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 === "respins") {
return { label: "Respins", variant: "gray", daysRemaining: null };
}
if (deadline.resolution === 'anulat') {
return { label: 'Anulat', variant: 'gray', daysRemaining: null };
if (deadline.resolution === "anulat") {
return { label: "Anulat", variant: "gray", daysRemaining: null };
}
return { label: 'Finalizat', variant: 'gray', daysRemaining: null };
return { label: "Finalizat", variant: "gray", daysRemaining: null };
}
// Pending — compute days remaining
@@ -83,16 +108,16 @@ export function getDeadlineDisplayStatus(deadline: TrackedDeadline): DeadlineDis
// Overdue + tacit applicable → tacit approval
if (daysRemaining < 0 && def?.tacitApprovalApplicable) {
return { label: 'Aprobat tacit', variant: 'blue', daysRemaining };
return { label: "Aprobat tacit", variant: "blue", daysRemaining };
}
if (daysRemaining < 0) {
return { label: 'Depășit termen', variant: 'red', daysRemaining };
return { label: "Depășit termen", variant: "red", daysRemaining };
}
if (daysRemaining <= 5) {
return { label: 'Urgent', variant: 'yellow', daysRemaining };
return { label: "Urgent", variant: "yellow", daysRemaining };
}
return { label: 'În termen', variant: 'green', daysRemaining };
return { label: "În termen", variant: "green", daysRemaining };
}
/**
@@ -103,25 +128,64 @@ export function aggregateDeadlines(entries: RegistryEntry[]): {
urgent: number;
overdue: number;
tacit: number;
all: Array<{ deadline: TrackedDeadline; entry: RegistryEntry; status: DeadlineDisplayStatus }>;
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 }> = [];
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') {
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') {
if (status.variant === "yellow") urgent++;
if (status.variant === "red") overdue++;
if (status.variant === "blue") tacit++;
} else if (dl.resolution === "aprobat-tacit") {
tacit++;
}
}
@@ -129,18 +193,26 @@ export function aggregateDeadlines(entries: RegistryEntry[]): {
// 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;
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, all };
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');
const m = String(d.getMonth() + 1).padStart(2, "0");
const day = String(d.getDate()).padStart(2, "0");
return `${y}-${m}-${day}`;
}