diff --git a/src/modules/registratura/components/imminent-actions.tsx b/src/modules/registratura/components/imminent-actions.tsx
new file mode 100644
index 0000000..8db7eda
--- /dev/null
+++ b/src/modules/registratura/components/imminent-actions.tsx
@@ -0,0 +1,218 @@
+"use client";
+
+import { useMemo } from "react";
+import { AlertTriangle, Bell, Clock, Timer, ChevronRight } from "lucide-react";
+import { Badge } from "@/shared/components/ui/badge";
+import { cn } from "@/shared/lib/utils";
+import type { RegistryEntry } from "../types";
+import { getDeadlineType } from "../services/deadline-catalog";
+
+interface ImminentActionsProps {
+ entries: RegistryEntry[];
+ onNavigate?: (entry: RegistryEntry) => void;
+}
+
+interface ActionItem {
+ entry: RegistryEntry;
+ type: "expiry" | "ac-validity" | "deadline-overdue" | "deadline-soon";
+ label: string;
+ detail: string;
+ daysLeft: number;
+ color: "red" | "amber" | "blue";
+}
+
+function formatDate(iso: string): string {
+ try {
+ return new Date(iso).toLocaleDateString("ro-RO", {
+ day: "2-digit",
+ month: "2-digit",
+ year: "numeric",
+ });
+ } catch {
+ return iso;
+ }
+}
+
+export function ImminentActions({ entries, onNavigate }: ImminentActionsProps) {
+ const actions = useMemo(() => {
+ const items: ActionItem[] = [];
+ const now = new Date();
+ now.setHours(0, 0, 0, 0);
+ const nowMs = now.getTime();
+
+ for (const entry of entries) {
+ if (entry.status !== "deschis") continue;
+
+ // Expiry date alerts
+ if (entry.expiryDate) {
+ const expiry = new Date(entry.expiryDate);
+ expiry.setHours(0, 0, 0, 0);
+ const daysLeft = Math.ceil((expiry.getTime() - nowMs) / (1000 * 60 * 60 * 24));
+ const alertDays = entry.expiryAlertDays ?? 30;
+
+ if (daysLeft < 0) {
+ items.push({
+ entry,
+ type: "expiry",
+ label: `Expirat de ${Math.abs(daysLeft)} zile`,
+ detail: `Valabilitate: ${formatDate(entry.expiryDate)}`,
+ daysLeft,
+ color: "red",
+ });
+ } else if (daysLeft <= alertDays) {
+ items.push({
+ entry,
+ type: "expiry",
+ label: `Expiră în ${daysLeft} zile`,
+ detail: `Valabilitate: ${formatDate(entry.expiryDate)}`,
+ daysLeft,
+ color: daysLeft <= 7 ? "red" : "amber",
+ });
+ }
+ }
+
+ // AC validity alerts
+ if (entry.acValidity?.enabled && !entry.acValidity.reminder.dismissed) {
+ 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);
+ validityEnd.setHours(0, 0, 0, 0);
+ const daysLeft = Math.ceil((validityEnd.getTime() - nowMs) / (1000 * 60 * 60 * 24));
+
+ if (daysLeft <= 90) {
+ items.push({
+ entry,
+ type: "ac-validity",
+ label: daysLeft < 0
+ ? `AC expirat de ${Math.abs(daysLeft)} zile`
+ : `AC expiră în ${daysLeft} zile`,
+ detail: `Valabilitate ${validityMonths} luni de la ${formatDate(ac.issuanceDate)}`,
+ daysLeft,
+ 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)
+ items.sort((a, b) => a.daysLeft - b.daysLeft);
+
+ return items;
+ }, [entries]);
+
+ if (actions.length === 0) return null;
+
+ const ICON_MAP = {
+ expiry: Timer,
+ "ac-validity": Bell,
+ "deadline-overdue": AlertTriangle,
+ "deadline-soon": Clock,
+ } as const;
+
+ const COLOR_MAP = {
+ red: {
+ border: "border-red-200 dark:border-red-900",
+ bg: "bg-red-50/60 dark:bg-red-950/20",
+ 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: {
+ border: "border-amber-200 dark:border-amber-900",
+ bg: "bg-amber-50/60 dark:bg-amber-950/20",
+ 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;
+
+ return (
+
+
+
+
+ Acțiuni iminente
+
+
+ {actions.length}
+
+
+
+ {actions.slice(0, 8).map((action, i) => {
+ const Icon = ICON_MAP[action.type];
+ const colors = COLOR_MAP[action.color];
+ return (
+
+ );
+ })}
+ {actions.length > 8 && (
+
+ + {actions.length - 8} alte acțiuni
+
+ )}
+
+
+ );
+}
diff --git a/src/modules/registratura/components/registratura-module.tsx b/src/modules/registratura/components/registratura-module.tsx
index 81ab94e..543d6af 100644
--- a/src/modules/registratura/components/registratura-module.tsx
+++ b/src/modules/registratura/components/registratura-module.tsx
@@ -41,6 +41,7 @@ import { DeadlineDashboard } from "./deadline-dashboard";
import { DeadlineConfigOverview } from "./deadline-config-overview";
import { ThreadExplorer } from "./thread-explorer";
import { CloseGuardDialog } from "./close-guard-dialog";
+import { ImminentActions } from "./imminent-actions";
import { getOverdueDays } from "../services/registry-service";
import {
aggregateDeadlines,
@@ -452,6 +453,13 @@ export function RegistraturaModule() {
+ {viewMode === "list" && (
+
+ )}
+
{viewMode === "list" && (
<>
diff --git a/src/modules/registratura/components/registry-entry-detail.tsx b/src/modules/registratura/components/registry-entry-detail.tsx
index 60577c5..1df9e6a 100644
--- a/src/modules/registratura/components/registry-entry-detail.tsx
+++ b/src/modules/registratura/components/registry-entry-detail.tsx
@@ -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({
+ {/* ── 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: ,
+ label: `AC — Remindere dezactivate`,
+ detail: `Valabilitate ${validityMonths} luni, luna ${currentMonth}/${validityMonths}`,
+ color: "text-muted-foreground",
+ });
+ } else if (daysLeft <= 30) {
+ alerts.push({
+ icon: ,
+ 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: ,
+ 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: ,
+ 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: ,
+ 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: ,
+ label: `Expiră în ${daysLeft} zile`,
+ detail: `Alertă setată la ${alertDays} zile înainte`,
+ color: "text-amber-600 dark:text-amber-400",
+ });
+ } else {
+ alerts.push({
+ icon: ,
+ 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: ,
+ 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: ,
+ 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 (
+
+
+ {alerts.map((alert, i) => (
+
+
{alert.icon}
+
+
{alert.label}
+
{alert.detail}
+
+
+ ))}
+
+
+ );
+ })()}
+
{/* ── Parties ── */}
diff --git a/src/modules/registratura/components/registry-entry-form.tsx b/src/modules/registratura/components/registry-entry-form.tsx
index 323bbb6..ac0097d 100644
--- a/src/modules/registratura/components/registry-entry-form.tsx
+++ b/src/modules/registratura/components/registry-entry-form.tsx
@@ -1307,6 +1307,28 @@ export function RegistryEntryForm({
onChange={(e) => setExpiryDate(e.target.value)}
className="mt-1"
/>
+ {date && (
+
+ {[6, 12, 24].map((months) => {
+ const base = new Date(date);
+ base.setMonth(base.getMonth() + months);
+ const iso = base.toISOString().slice(0, 10);
+ const isSelected = expiryDate === iso;
+ return (
+
+ );
+ })}
+
+ )}
{expiryDate && (
diff --git a/src/modules/registratura/components/thread-explorer.tsx b/src/modules/registratura/components/thread-explorer.tsx
index c5a75f3..77b848b 100644
--- a/src/modules/registratura/components/thread-explorer.tsx
+++ b/src/modules/registratura/components/thread-explorer.tsx
@@ -126,7 +126,7 @@ export function ThreadExplorer({
onNavigateEntry,
}: ThreadExplorerProps) {
const [search, setSearch] = useState("");
- const [activeOnly, setActiveOnly] = useState(false);
+ const [activeOnly, setActiveOnly] = useState(true);
const threads = useMemo(() => buildThreads(entries), [entries]);