refactor(registratura): focus imminent actions on expiry/AC only, remove deadlines

Strip institutional legal deadlines from the dashboard — show only documents
expiring and AC validity within 60-day horizon. Rename to "De reînnoit".

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
AI Assistant
2026-03-13 09:47:19 +02:00
parent 8275ed1d95
commit 0cd28de733
@@ -1,11 +1,10 @@
"use client"; "use client";
import { useMemo } from "react"; import { useMemo } from "react";
import { AlertTriangle, Bell, Clock, Timer, ChevronRight } from "lucide-react"; import { AlertTriangle, Bell, Timer, ChevronRight } from "lucide-react";
import { Badge } from "@/shared/components/ui/badge"; import { Badge } from "@/shared/components/ui/badge";
import { cn } from "@/shared/lib/utils"; import { cn } from "@/shared/lib/utils";
import type { RegistryEntry } from "../types"; import type { RegistryEntry } from "../types";
import { getDeadlineType } from "../services/deadline-catalog";
interface ImminentActionsProps { interface ImminentActionsProps {
entries: RegistryEntry[]; entries: RegistryEntry[];
@@ -14,13 +13,16 @@ interface ImminentActionsProps {
interface ActionItem { interface ActionItem {
entry: RegistryEntry; entry: RegistryEntry;
type: "expiry" | "ac-validity" | "deadline-overdue" | "deadline-soon"; type: "expiry" | "ac-validity";
label: string; label: string;
detail: string; detail: string;
daysLeft: number; daysLeft: number;
color: "red" | "amber" | "blue"; color: "red" | "amber";
} }
/** Max lookahead window in days */
const HORIZON_DAYS = 60;
function formatDate(iso: string): string { function formatDate(iso: string): string {
try { try {
return new Date(iso).toLocaleDateString("ro-RO", { return new Date(iso).toLocaleDateString("ro-RO", {
@@ -43,35 +45,34 @@ export function ImminentActions({ entries, onNavigate }: ImminentActionsProps) {
for (const entry of entries) { for (const entry of entries) {
if (entry.status !== "deschis") continue; if (entry.status !== "deschis") continue;
// Expiry date alerts // Document expiry alerts (already expired or within horizon)
if (entry.expiryDate) { if (entry.expiryDate) {
const expiry = new Date(entry.expiryDate); const expiry = new Date(entry.expiryDate);
expiry.setHours(0, 0, 0, 0); expiry.setHours(0, 0, 0, 0);
const daysLeft = Math.ceil((expiry.getTime() - nowMs) / (1000 * 60 * 60 * 24)); const daysLeft = Math.ceil((expiry.getTime() - nowMs) / (1000 * 60 * 60 * 24));
const alertDays = entry.expiryAlertDays ?? 30;
if (daysLeft < 0) { if (daysLeft < 0) {
items.push({ items.push({
entry, entry,
type: "expiry", type: "expiry",
label: `Expirat de ${Math.abs(daysLeft)} zile`, label: `Expirat de ${Math.abs(daysLeft)} zile`,
detail: `Valabilitate: ${formatDate(entry.expiryDate)}`, detail: `Valabil până: ${formatDate(entry.expiryDate)}`,
daysLeft, daysLeft,
color: "red", color: "red",
}); });
} else if (daysLeft <= alertDays) { } else if (daysLeft <= HORIZON_DAYS) {
items.push({ items.push({
entry, entry,
type: "expiry", type: "expiry",
label: `Expiră în ${daysLeft} zile`, label: daysLeft === 0 ? "Expiră azi" : `Expiră în ${daysLeft} zile`,
detail: `Valabilitate: ${formatDate(entry.expiryDate)}`, detail: `Valabil până: ${formatDate(entry.expiryDate)}`,
daysLeft, daysLeft,
color: daysLeft <= 7 ? "red" : "amber", color: daysLeft <= 14 ? "red" : "amber",
}); });
} }
} }
// AC validity alerts // AC validity alerts (already expired or within horizon)
if (entry.acValidity?.enabled && !entry.acValidity.reminder.dismissed) { if (entry.acValidity?.enabled && !entry.acValidity.reminder.dismissed) {
const ac = entry.acValidity; const ac = entry.acValidity;
const validityMonths = ac.validityMonths ?? 12; const validityMonths = ac.validityMonths ?? 12;
@@ -81,54 +82,30 @@ export function ImminentActions({ entries, onNavigate }: ImminentActionsProps) {
validityEnd.setHours(0, 0, 0, 0); validityEnd.setHours(0, 0, 0, 0);
const daysLeft = Math.ceil((validityEnd.getTime() - nowMs) / (1000 * 60 * 60 * 24)); const daysLeft = Math.ceil((validityEnd.getTime() - nowMs) / (1000 * 60 * 60 * 24));
if (daysLeft <= 90) { if (daysLeft < 0) {
items.push({ items.push({
entry, entry,
type: "ac-validity", type: "ac-validity",
label: daysLeft < 0 label: `AC expirat de ${Math.abs(daysLeft)} zile`,
? `AC expirat de ${Math.abs(daysLeft)} zile` detail: `${validityMonths} luni de la ${formatDate(ac.issuanceDate)}`,
: `AC expiră în ${daysLeft} zile`, daysLeft,
detail: `Valabilitate ${validityMonths} luni de la ${formatDate(ac.issuanceDate)}`, color: "red",
});
} else if (daysLeft <= HORIZON_DAYS) {
items.push({
entry,
type: "ac-validity",
label: daysLeft === 0 ? "AC expiră azi" : `AC expiră în ${daysLeft} zile`,
detail: `${validityMonths} luni de la ${formatDate(ac.issuanceDate)}`,
daysLeft, daysLeft,
color: daysLeft <= 30 ? "red" : "amber", color: daysLeft <= 30 ? "red" : "amber",
}); });
} }
} }
// Tracked deadlines
const pending = (entry.trackedDeadlines ?? []).filter(d => d.resolution === "pending");
for (const dl of pending) {
const due = new Date(dl.dueDate);
due.setHours(0, 0, 0, 0);
const daysLeft = Math.ceil((due.getTime() - nowMs) / (1000 * 60 * 60 * 24));
const typeDef = getDeadlineType(dl.typeId);
const typeLabel = typeDef?.label ?? dl.typeId;
if (daysLeft < 0) {
items.push({
entry,
type: "deadline-overdue",
label: `Termen depășit: ${typeLabel}`,
detail: `Scadent: ${formatDate(dl.dueDate)} (${Math.abs(daysLeft)} zile depășit)`,
daysLeft,
color: "red",
});
} else if (daysLeft <= 5) {
items.push({
entry,
type: "deadline-soon",
label: `Termen iminent: ${typeLabel}`,
detail: `Scadent: ${formatDate(dl.dueDate)} (${daysLeft === 0 ? "azi" : `${daysLeft} zile`})`,
daysLeft,
color: daysLeft <= 1 ? "red" : "amber",
});
}
}
} }
// Sort: most urgent first (most negative daysLeft) // Most urgent first
items.sort((a, b) => a.daysLeft - b.daysLeft); items.sort((a, b) => a.daysLeft - b.daysLeft);
return items; return items;
}, [entries]); }, [entries]);
@@ -137,8 +114,6 @@ export function ImminentActions({ entries, onNavigate }: ImminentActionsProps) {
const ICON_MAP = { const ICON_MAP = {
expiry: Timer, expiry: Timer,
"ac-validity": Bell, "ac-validity": Bell,
"deadline-overdue": AlertTriangle,
"deadline-soon": Clock,
} as const; } as const;
const COLOR_MAP = { const COLOR_MAP = {
@@ -146,19 +121,11 @@ export function ImminentActions({ entries, onNavigate }: ImminentActionsProps) {
border: "border-red-200 dark:border-red-900", border: "border-red-200 dark:border-red-900",
bg: "bg-red-50/60 dark:bg-red-950/20", bg: "bg-red-50/60 dark:bg-red-950/20",
text: "text-red-700 dark:text-red-400", text: "text-red-700 dark:text-red-400",
badge: "bg-red-100 text-red-700 dark:bg-red-900/40 dark:text-red-400",
}, },
amber: { amber: {
border: "border-amber-200 dark:border-amber-900", border: "border-amber-200 dark:border-amber-900",
bg: "bg-amber-50/60 dark:bg-amber-950/20", bg: "bg-amber-50/60 dark:bg-amber-950/20",
text: "text-amber-700 dark:text-amber-400", text: "text-amber-700 dark:text-amber-400",
badge: "bg-amber-100 text-amber-700 dark:bg-amber-900/40 dark:text-amber-400",
},
blue: {
border: "border-blue-200 dark:border-blue-900",
bg: "bg-blue-50/60 dark:bg-blue-950/20",
text: "text-blue-700 dark:text-blue-400",
badge: "bg-blue-100 text-blue-700 dark:bg-blue-900/40 dark:text-blue-400",
}, },
} as const; } as const;
@@ -167,14 +134,17 @@ export function ImminentActions({ entries, onNavigate }: ImminentActionsProps) {
<div className="flex items-center gap-2 mb-2.5"> <div className="flex items-center gap-2 mb-2.5">
<AlertTriangle className="h-4 w-4 text-amber-600 dark:text-amber-400" /> <AlertTriangle className="h-4 w-4 text-amber-600 dark:text-amber-400" />
<span className="text-sm font-semibold text-amber-800 dark:text-amber-300"> <span className="text-sm font-semibold text-amber-800 dark:text-amber-300">
Acțiuni iminente De reînnoit
</span> </span>
<Badge className="text-[10px] px-1.5 py-0 bg-amber-200 text-amber-800 dark:bg-amber-900/50 dark:text-amber-300"> <Badge className="text-[10px] px-1.5 py-0 bg-amber-200 text-amber-800 dark:bg-amber-900/50 dark:text-amber-300">
{actions.length} {actions.length}
</Badge> </Badge>
<span className="ml-auto text-[10px] text-muted-foreground">
următoarele {HORIZON_DAYS} zile
</span>
</div> </div>
<div className="space-y-1.5"> <div className="space-y-1.5">
{actions.slice(0, 8).map((action, i) => { {actions.map((action, i) => {
const Icon = ICON_MAP[action.type]; const Icon = ICON_MAP[action.type];
const colors = COLOR_MAP[action.color]; const colors = COLOR_MAP[action.color];
return ( return (
@@ -199,19 +169,13 @@ export function ImminentActions({ entries, onNavigate }: ImminentActionsProps) {
</span> </span>
</div> </div>
<p className="text-[10px] text-muted-foreground truncate"> <p className="text-[10px] text-muted-foreground truncate">
{action.entry.subject} {action.entry.subject} {action.detail}
{action.detail ? `${action.detail}` : ""}
</p> </p>
</div> </div>
<ChevronRight className="h-3.5 w-3.5 text-muted-foreground shrink-0" /> <ChevronRight className="h-3.5 w-3.5 text-muted-foreground shrink-0" />
</button> </button>
); );
})} })}
{actions.length > 8 && (
<p className="text-[10px] text-muted-foreground text-center pt-1">
+ {actions.length - 8} alte acțiuni
</p>
)}
</div> </div>
</div> </div>
); );