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:
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user