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:
AI Assistant
2026-02-28 04:31:32 +02:00
parent 85bdb59da4
commit 99fbdddb68
9 changed files with 649 additions and 150 deletions
@@ -10,6 +10,9 @@ import {
Info,
GitBranch,
Loader2,
AlertTriangle,
Calendar,
Globe,
} from "lucide-react";
import type { CompanyId } from "@/core/auth/types";
import type {
@@ -149,6 +152,24 @@ export function RegistryEntryForm({
const [linkedSearch, setLinkedSearch] = useState("");
const [threadSearch, setThreadSearch] = useState("");
// ── 3.03 new fields ──
const [recipientRegNumber, setRecipientRegNumber] = useState(
initial?.recipientRegNumber ?? "",
);
const [recipientRegDate, setRecipientRegDate] = useState(
initial?.recipientRegDate ?? "",
);
const [expiryDate, setExpiryDate] = useState(initial?.expiryDate ?? "");
const [expiryAlertDays, setExpiryAlertDays] = useState(
initial?.expiryAlertDays ?? 30,
);
const [externalStatusUrl, setExternalStatusUrl] = useState(
initial?.externalStatusUrl ?? "",
);
const [externalTrackingId, setExternalTrackingId] = useState(
initial?.externalTrackingId ?? "",
);
// ── Submission lock + file upload tracking ──
const [isSubmitting, setIsSubmitting] = useState(false);
const [uploadingCount, setUploadingCount] = useState(0);
@@ -340,6 +361,12 @@ export function RegistryEntryForm({
assignee: assignee || undefined,
assigneeContactId: assigneeContactId || undefined,
threadParentId: threadParentId || undefined,
recipientRegNumber: recipientRegNumber || undefined,
recipientRegDate: recipientRegDate || undefined,
expiryDate: expiryDate || undefined,
expiryAlertDays: expiryDate ? expiryAlertDays : undefined,
externalStatusUrl: externalStatusUrl || undefined,
externalTrackingId: externalTrackingId || undefined,
linkedEntryIds,
attachments,
trackedDeadlines:
@@ -595,7 +622,59 @@ export function RegistryEntryForm({
</div>
</div>
{/* Assignee (Responsabil) */}
{/* Recipient registration fields — only for outgoing (iesit) documents */}
{direction === "iesit" && (
<div className="rounded-md border border-blue-500/30 bg-blue-500/5 p-3 space-y-3">
<div className="flex items-center gap-2">
<AlertTriangle className="h-4 w-4 text-blue-600 dark:text-blue-400" />
<Label className="text-sm font-medium text-blue-700 dark:text-blue-300">
Înregistrare la destinatar
</Label>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Info className="h-3.5 w-3.5 text-muted-foreground cursor-help" />
</TooltipTrigger>
<TooltipContent side="right" className="max-w-xs">
<p className="text-xs">
Termenul legal pentru ieșiri curge DOAR de la data
înregistrării la destinatar, nu de la data trimiterii.
Completează aceste câmpuri când primești confirmarea.
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<div className="grid gap-3 sm:grid-cols-2">
<div>
<Label className="text-xs">Nr. înregistrare destinatar</Label>
<Input
value={recipientRegNumber}
onChange={(e) => setRecipientRegNumber(e.target.value)}
className="mt-1"
placeholder="Ex: 12345/2026"
/>
</div>
<div>
<Label className="text-xs">Data înregistrare destinatar</Label>
<Input
type="date"
value={recipientRegDate}
onChange={(e) => setRecipientRegDate(e.target.value)}
className="mt-1"
/>
</div>
</div>
{!recipientRegNumber && !recipientRegDate && (
<p className="text-[10px] text-amber-600 dark:text-amber-400">
Atenție: Datele de la destinatar nu sunt completate. Termenele
legale atașate nu vor porni până la completarea lor.
</p>
)}
</div>
)}
{/* Assignee (Responsabil) — kept below */}
<div className="relative">
<Label className="flex items-center gap-1.5">
Responsabil
@@ -707,6 +786,104 @@ export function RegistryEntryForm({
</div>
</div>
{/* Document Expiry — CU/AC validity tracking */}
<div className="rounded-md border border-muted p-3 space-y-3">
<Label className="flex items-center gap-1.5 text-sm font-medium">
<Calendar className="h-3.5 w-3.5" />
Valabilitate document
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Info className="h-3.5 w-3.5 text-muted-foreground cursor-help" />
</TooltipTrigger>
<TooltipContent side="right" className="max-w-xs">
<p className="text-xs">
Pentru documente cu termen de valabilitate (CU, AC etc.).
Sistemul va genera alerte înainte de expirare.
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</Label>
<div className="grid gap-3 sm:grid-cols-2">
<div>
<Label className="text-xs">Data expirare</Label>
<Input
type="date"
value={expiryDate}
onChange={(e) => setExpiryDate(e.target.value)}
className="mt-1"
/>
</div>
{expiryDate && (
<div>
<Label className="text-xs">Alertă cu X zile înainte</Label>
<Input
type="number"
value={expiryAlertDays}
onChange={(e) =>
setExpiryAlertDays(parseInt(e.target.value, 10) || 30)
}
className="mt-1"
min={1}
max={365}
/>
</div>
)}
</div>
{expiryDate &&
(() => {
const expiry = new Date(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),
);
if (daysLeft < 0) {
return (
<p className="text-[10px] text-red-600 dark:text-red-400 font-medium">
Document expirat de {Math.abs(daysLeft)} zile!
</p>
);
}
if (daysLeft <= expiryAlertDays) {
return (
<p className="text-[10px] text-amber-600 dark:text-amber-400">
Expiră în {daysLeft} zile inițiați procedurile de
prelungire.
</p>
);
}
return null;
})()}
</div>
{/* Web scraping prep — external tracking */}
<div className="grid gap-4 sm:grid-cols-2">
<div>
<Label className="flex items-center gap-1.5 text-xs">
<Globe className="h-3 w-3" />
URL verificare status (opțional)
</Label>
<Input
value={externalStatusUrl}
onChange={(e) => setExternalStatusUrl(e.target.value)}
className="mt-1 text-xs"
placeholder="https://portal.primaria.ro/..."
/>
</div>
<div>
<Label className="text-xs">ID urmărire extern (opțional)</Label>
<Input
value={externalTrackingId}
onChange={(e) => setExternalTrackingId(e.target.value)}
className="mt-1 text-xs"
placeholder="Ex: REF-2026-001"
/>
</div>
</div>
{/* Thread parent — reply to another entry */}
{allEntries && allEntries.length > 0 && (
<div>