feat(registratura): add reminders section, expiry helpers, imminent actions dashboard

- Add "Remindere & Alerte" section in entry detail panel showing AC validity,
  expiry date, and tracked deadline status with color-coded indicators
- Add quick expiry date buttons (6/12/24 months from document date) in entry form
- Default dosare view to show only active threads
- Add ImminentActions dashboard component showing urgent items (expired docs,
  AC validity warnings, overdue/imminent deadlines) sorted by urgency

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
AI Assistant
2026-03-13 09:42:54 +02:00
parent 22eb9a4383
commit 8275ed1d95
5 changed files with 398 additions and 1 deletions
@@ -3,6 +3,8 @@
import {
ArrowDownToLine,
ArrowUpFromLine,
Bell,
BellOff,
Calendar,
CheckCircle2,
Clock,
@@ -24,6 +26,9 @@ import {
RefreshCw,
ChevronDown,
ChevronUp,
AlertTriangle,
ShieldAlert,
Timer,
} from "lucide-react";
import { Button } from "@/shared/components/ui/button";
import { Badge } from "@/shared/components/ui/badge";
@@ -50,6 +55,7 @@ import {
} from "./attachment-preview";
import { findAuthorityForContact } from "../services/authority-catalog";
import { computeTransmissionStatus } from "../services/deadline-service";
import { getDeadlineType } from "../services/deadline-catalog";
import { StatusMonitorConfig } from "./status-monitor-config";
import { FlowDiagram } from "./flow-diagram";
import { DeadlineTimeline } from "./deadline-timeline";
@@ -399,6 +405,149 @@ export function RegistryEntryDetail({
</div>
</DetailSection>
{/* ── Reminders / Alerts summary ── */}
{(() => {
const alerts: { icon: React.ReactNode; label: string; detail: string; color: string }[] = [];
// AC validity reminder
if (entry.acValidity?.enabled) {
const ac = entry.acValidity;
const validityMonths = ac.validityMonths ?? 12;
const issuance = new Date(ac.issuanceDate);
const validityEnd = new Date(issuance);
validityEnd.setMonth(validityEnd.getMonth() + validityMonths);
const now = new Date();
now.setHours(0, 0, 0, 0);
validityEnd.setHours(0, 0, 0, 0);
const daysLeft = Math.ceil((validityEnd.getTime() - now.getTime()) / (1000 * 60 * 60 * 24));
const monthsLeft = Math.ceil(daysLeft / 30);
const currentMonth = Math.max(1, validityMonths - monthsLeft + 1);
if (ac.reminder.dismissed) {
alerts.push({
icon: <BellOff className="h-3.5 w-3.5" />,
label: `AC — Remindere dezactivate`,
detail: `Valabilitate ${validityMonths} luni, luna ${currentMonth}/${validityMonths}`,
color: "text-muted-foreground",
});
} else if (daysLeft <= 30) {
alerts.push({
icon: <ShieldAlert className="h-3.5 w-3.5" />,
label: `AC — Expiră în ${daysLeft} zile!`,
detail: `Luna ${currentMonth}/${validityMonths} — acțiune imediată`,
color: "text-red-600 dark:text-red-400",
});
} else if (daysLeft <= 90) {
alerts.push({
icon: <Bell className="h-3.5 w-3.5" />,
label: `AC — Reminder activ (luna ${currentMonth}/${validityMonths})`,
detail: `Mai sunt ${daysLeft} zile (${monthsLeft} luni)`,
color: "text-amber-600 dark:text-amber-400",
});
} else {
alerts.push({
icon: <Bell className="h-3.5 w-3.5" />,
label: `AC — Reminder activ (luna ${currentMonth}/${validityMonths})`,
detail: `Mai sunt ${daysLeft} zile`,
color: "text-emerald-600 dark:text-emerald-400",
});
}
if (ac.reminder.snoozeCount > 0) {
const lastAlert = alerts[alerts.length - 1];
if (lastAlert) {
lastAlert.detail += ` · Amânat de ${ac.reminder.snoozeCount}×`;
}
}
}
// Expiry date alert
if (entry.expiryDate && entry.status === "deschis") {
const expiry = new Date(entry.expiryDate);
const now = new Date();
now.setHours(0, 0, 0, 0);
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 < 0) {
alerts.push({
icon: <AlertTriangle className="h-3.5 w-3.5" />,
label: `Document expirat de ${Math.abs(daysLeft)} zile`,
detail: `Expirat: ${formatDate(entry.expiryDate)}`,
color: "text-red-600 dark:text-red-400",
});
} else if (daysLeft <= alertDays) {
alerts.push({
icon: <Timer className="h-3.5 w-3.5" />,
label: `Expiră în ${daysLeft} zile`,
detail: `Alertă setată la ${alertDays} zile înainte`,
color: "text-amber-600 dark:text-amber-400",
});
} else {
alerts.push({
icon: <Timer className="h-3.5 w-3.5" />,
label: `Valabil — expiră ${formatDate(entry.expiryDate)}`,
detail: `Mai sunt ${daysLeft} zile (alertă la ${alertDays} zile)`,
color: "text-emerald-600 dark:text-emerald-400",
});
}
}
// Tracked deadlines summary
const pendingDeadlines = (entry.trackedDeadlines ?? []).filter(d => d.resolution === "pending");
if (pendingDeadlines.length > 0) {
const overdueCount = pendingDeadlines.filter(d => {
const due = new Date(d.dueDate);
due.setHours(0, 0, 0, 0);
return due.getTime() < new Date().setHours(0, 0, 0, 0);
}).length;
if (overdueCount > 0) {
alerts.push({
icon: <AlertTriangle className="h-3.5 w-3.5" />,
label: `${overdueCount} ${overdueCount === 1 ? "termen depășit" : "termene depășite"}`,
detail: `${pendingDeadlines.length} ${pendingDeadlines.length === 1 ? "termen activ" : "termene active"} total`,
color: "text-red-600 dark:text-red-400",
});
} else {
alerts.push({
icon: <Clock className="h-3.5 w-3.5" />,
label: `${pendingDeadlines.length} ${pendingDeadlines.length === 1 ? "termen activ" : "termene active"}`,
detail: pendingDeadlines.map(d => `${getDeadlineType(d.typeId)?.label ?? d.typeId} (${formatDate(d.dueDate)})`).join(", "),
color: "text-blue-600 dark:text-blue-400",
});
}
}
if (alerts.length === 0) return null;
return (
<DetailSection title="Remindere & Alerte">
<div className="space-y-2">
{alerts.map((alert, i) => (
<div
key={i}
className={cn(
"flex items-start gap-2.5 rounded-md border px-3 py-2",
alert.color.includes("red") && "border-red-200 bg-red-50/50 dark:border-red-900 dark:bg-red-950/20",
alert.color.includes("amber") && "border-amber-200 bg-amber-50/50 dark:border-amber-900 dark:bg-amber-950/20",
alert.color.includes("emerald") && "border-emerald-200 bg-emerald-50/50 dark:border-emerald-900 dark:bg-emerald-950/20",
alert.color.includes("blue") && "border-blue-200 bg-blue-50/50 dark:border-blue-900 dark:bg-blue-950/20",
alert.color.includes("muted") && "border-muted",
)}
>
<span className={cn("mt-0.5 shrink-0", alert.color)}>{alert.icon}</span>
<div className="min-w-0">
<p className={cn("text-sm font-medium", alert.color)}>{alert.label}</p>
<p className="text-[10px] text-muted-foreground">{alert.detail}</p>
</div>
</div>
))}
</div>
</DetailSection>
);
})()}
{/* ── Parties ── */}
<DetailSection title="Părți">
<div className="grid grid-cols-2 gap-3">