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:
@@ -15,6 +15,20 @@ export const DEADLINE_CATALOG: DeadlineTypeDef[] = [
|
||||
category: "certificat",
|
||||
legalReference: "Legea 50/1991, art. 6¹",
|
||||
},
|
||||
{
|
||||
id: "prelungire-cu",
|
||||
label: "Cerere prelungire CU",
|
||||
description:
|
||||
"Cerere de prelungire a Certificatului de Urbanism. Se depune înainte de expirare.",
|
||||
days: 15,
|
||||
dayType: "calendar",
|
||||
startDateLabel: "Data depunerii cererii de prelungire",
|
||||
requiresCustomStartDate: true,
|
||||
startDateHint: "Data la care s-a depus cererea de prelungire a CU",
|
||||
tacitApprovalApplicable: true,
|
||||
category: "certificat",
|
||||
legalReference: "Legea 50/1991, art. 6¹",
|
||||
},
|
||||
|
||||
// ── Avize ──
|
||||
{
|
||||
|
||||
@@ -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}`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user