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
@@ -1,12 +1,13 @@
'use client';
"use client";
import { Clock, CheckCircle2, X } from 'lucide-react';
import { Badge } from '@/shared/components/ui/badge';
import { Button } from '@/shared/components/ui/button';
import type { TrackedDeadline } from '../types';
import { getDeadlineType } from '../services/deadline-catalog';
import { getDeadlineDisplayStatus } from '../services/deadline-service';
import { cn } from '@/shared/lib/utils';
import { useState } from "react";
import { Clock, CheckCircle2, X, History } from "lucide-react";
import { Badge } from "@/shared/components/ui/badge";
import { Button } from "@/shared/components/ui/button";
import type { TrackedDeadline } from "../types";
import { getDeadlineType } from "../services/deadline-catalog";
import { getDeadlineDisplayStatus } from "../services/deadline-service";
import { cn } from "@/shared/lib/utils";
interface DeadlineCardProps {
deadline: TrackedDeadline;
@@ -15,78 +16,156 @@ interface DeadlineCardProps {
}
const VARIANT_CLASSES: Record<string, string> = {
green: 'border-green-500/30 bg-green-50 dark:bg-green-950/20',
yellow: 'border-yellow-500/30 bg-yellow-50 dark:bg-yellow-950/20',
red: 'border-red-500/30 bg-red-50 dark:bg-red-950/20',
blue: 'border-blue-500/30 bg-blue-50 dark:bg-blue-950/20',
gray: 'border-muted bg-muted/30',
green: "border-green-500/30 bg-green-50 dark:bg-green-950/20",
yellow: "border-yellow-500/30 bg-yellow-50 dark:bg-yellow-950/20",
red: "border-red-500/30 bg-red-50 dark:bg-red-950/20",
blue: "border-blue-500/30 bg-blue-50 dark:bg-blue-950/20",
gray: "border-muted bg-muted/30",
};
const BADGE_CLASSES: Record<string, string> = {
green: 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200',
yellow: 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200',
red: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200',
blue: 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200',
gray: 'bg-muted text-muted-foreground',
green: "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200",
yellow:
"bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200",
red: "bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200",
blue: "bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200",
gray: "bg-muted text-muted-foreground",
};
export function DeadlineCard({ deadline, onResolve, onRemove }: DeadlineCardProps) {
export function DeadlineCard({
deadline,
onResolve,
onRemove,
}: DeadlineCardProps) {
const def = getDeadlineType(deadline.typeId);
const status = getDeadlineDisplayStatus(deadline);
const [showAudit, setShowAudit] = useState(false);
const auditLog = deadline.auditLog ?? [];
return (
<div className={cn('flex items-center gap-3 rounded-lg border p-3', VARIANT_CLASSES[status.variant] ?? '')}>
<Clock className="h-4 w-4 shrink-0 text-muted-foreground" />
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<span className="text-sm font-medium truncate">{def?.label ?? deadline.typeId}</span>
<Badge className={cn('text-[10px] border-0', BADGE_CLASSES[status.variant] ?? '')}>
{status.label}
{status.daysRemaining !== null && status.variant !== 'blue' && (
<span className="ml-1">
({status.daysRemaining < 0 ? `${Math.abs(status.daysRemaining)}z depășit` : `${status.daysRemaining}z`})
</span>
<div
className={cn(
"rounded-lg border p-3",
VARIANT_CLASSES[status.variant] ?? "",
)}
>
<div className="flex items-center gap-3">
<Clock className="h-4 w-4 shrink-0 text-muted-foreground" />
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<span className="text-sm font-medium truncate">
{def?.label ?? deadline.typeId}
</span>
<Badge
className={cn(
"text-[10px] border-0",
BADGE_CLASSES[status.variant] ?? "",
)}
>
{status.label}
{status.daysRemaining !== null && status.variant !== "blue" && (
<span className="ml-1">
(
{status.daysRemaining < 0
? `${Math.abs(status.daysRemaining)}z depășit`
: `${status.daysRemaining}z`}
)
</span>
)}
</Badge>
</div>
<div className="text-xs text-muted-foreground mt-0.5">
{def?.isBackwardDeadline ? "Termen limită" : "Start"}:{" "}
{formatDate(deadline.startDate)}
{" → "}
{def?.isBackwardDeadline ? "Depunere până la" : "Termen"}:{" "}
{formatDate(deadline.dueDate)}
{def?.dayType === "working" && (
<span className="ml-1">(zile lucrătoare)</span>
)}
</Badge>
</div>
</div>
<div className="text-xs text-muted-foreground mt-0.5">
{def?.isBackwardDeadline ? 'Termen limită' : 'Start'}: {formatDate(deadline.startDate)}
{' → '}
{def?.isBackwardDeadline ? 'Depunere până la' : 'Termen'}: {formatDate(deadline.dueDate)}
{def?.dayType === 'working' && <span className="ml-1">(zile lucrătoare)</span>}
</div>
</div>
<div className="flex gap-1 shrink-0">
{deadline.resolution === 'pending' && (
<div className="flex gap-1 shrink-0">
{auditLog.length > 0 && (
<Button
type="button"
variant="ghost"
size="icon"
className="h-7 w-7 text-muted-foreground"
onClick={() => setShowAudit(!showAudit)}
title="Istoric modificări"
>
<History className="h-3.5 w-3.5" />
</Button>
)}
{deadline.resolution === "pending" && (
<Button
type="button"
variant="ghost"
size="icon"
className="h-7 w-7 text-green-600"
onClick={() => onResolve(deadline)}
title="Rezolvă"
>
<CheckCircle2 className="h-3.5 w-3.5" />
</Button>
)}
<Button
type="button"
variant="ghost"
size="icon"
className="h-7 w-7 text-green-600"
onClick={() => onResolve(deadline)}
title="Rezolvă"
className="h-7 w-7 text-destructive"
onClick={() => onRemove(deadline.id)}
title="Șterge"
>
<CheckCircle2 className="h-3.5 w-3.5" />
<X className="h-3.5 w-3.5" />
</Button>
)}
<Button
type="button"
variant="ghost"
size="icon"
className="h-7 w-7 text-destructive"
onClick={() => onRemove(deadline.id)}
title="Șterge"
>
<X className="h-3.5 w-3.5" />
</Button>
</div>
</div>
{/* Audit log */}
{showAudit && auditLog.length > 0 && (
<div className="mt-2 border-t pt-2 space-y-1">
<p className="text-[10px] font-medium text-muted-foreground uppercase tracking-wider">
Istoric modificări
</p>
{auditLog.map((entry, i) => (
<div key={i} className="flex items-start gap-2 text-[11px]">
<span className="text-muted-foreground whitespace-nowrap">
{formatDateTime(entry.timestamp)}
</span>
{entry.actor && (
<span className="font-medium">{entry.actor}</span>
)}
<span className="text-muted-foreground">{entry.detail}</span>
</div>
))}
</div>
)}
</div>
);
}
function formatDate(iso: string): string {
try {
return new Date(iso).toLocaleDateString('ro-RO', { day: '2-digit', month: '2-digit', year: 'numeric' });
return new Date(iso).toLocaleDateString("ro-RO", {
day: "2-digit",
month: "2-digit",
year: "numeric",
});
} catch {
return iso;
}
}
function formatDateTime(iso: string): string {
try {
return new Date(iso).toLocaleString("ro-RO", {
day: "2-digit",
month: "2-digit",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
});
} catch {
return iso;
}
@@ -1,55 +1,83 @@
'use client';
"use client";
import { useState, useMemo } from 'react';
import { Card, CardContent } from '@/shared/components/ui/card';
import { Badge } from '@/shared/components/ui/badge';
import { Label } from '@/shared/components/ui/label';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/shared/components/ui/select';
import { Button } from '@/shared/components/ui/button';
import type { RegistryEntry, TrackedDeadline, DeadlineResolution, DeadlineCategory } from '../types';
import { aggregateDeadlines } from '../services/deadline-service';
import { CATEGORY_LABELS, getDeadlineType } from '../services/deadline-catalog';
import { useDeadlineFilters } from '../hooks/use-deadline-filters';
import { DeadlineTable } from './deadline-table';
import { DeadlineResolveDialog } from './deadline-resolve-dialog';
import { useState, useMemo } from "react";
import { Card, CardContent } from "@/shared/components/ui/card";
import { Badge } from "@/shared/components/ui/badge";
import { Label } from "@/shared/components/ui/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/shared/components/ui/select";
import { Button } from "@/shared/components/ui/button";
import type {
RegistryEntry,
TrackedDeadline,
DeadlineResolution,
DeadlineCategory,
} from "../types";
import { aggregateDeadlines } from "../services/deadline-service";
import { CATEGORY_LABELS, getDeadlineType } from "../services/deadline-catalog";
import { useDeadlineFilters } from "../hooks/use-deadline-filters";
import { DeadlineTable } from "./deadline-table";
import { DeadlineResolveDialog } from "./deadline-resolve-dialog";
interface DeadlineDashboardProps {
entries: RegistryEntry[];
onResolveDeadline: (entryId: string, deadlineId: string, resolution: DeadlineResolution, note: string, chainNext: boolean) => void;
onAddChainedDeadline: (entryId: string, typeId: string, startDate: string, parentId: string) => void;
onResolveDeadline: (
entryId: string,
deadlineId: string,
resolution: DeadlineResolution,
note: string,
chainNext: boolean,
) => void;
onAddChainedDeadline: (
entryId: string,
typeId: string,
startDate: string,
parentId: string,
) => void;
}
const RESOLUTION_LABELS: Record<string, string> = {
pending: 'În așteptare',
completed: 'Finalizat',
'aprobat-tacit': 'Aprobat tacit',
respins: 'Respins',
anulat: 'Anulat',
pending: "În așteptare",
completed: "Finalizat",
"aprobat-tacit": "Aprobat tacit",
respins: "Respins",
anulat: "Anulat",
};
export function DeadlineDashboard({ entries, onResolveDeadline, onAddChainedDeadline }: DeadlineDashboardProps) {
export function DeadlineDashboard({
entries,
onResolveDeadline,
onAddChainedDeadline,
}: DeadlineDashboardProps) {
const { filters, updateFilter } = useDeadlineFilters();
const [resolvingEntry, setResolvingEntry] = useState<string | null>(null);
const [resolvingDeadline, setResolvingDeadline] = useState<TrackedDeadline | null>(null);
const [resolvingDeadline, setResolvingDeadline] =
useState<TrackedDeadline | null>(null);
const stats = useMemo(() => aggregateDeadlines(entries), [entries]);
const filteredRows = useMemo(() => {
return stats.all.filter((row) => {
if (filters.category !== 'all') {
if (filters.category !== "all") {
const def = getDeadlineType(row.deadline.typeId);
if (def && def.category !== filters.category) return false;
}
if (filters.resolution !== 'all') {
if (filters.resolution !== "all") {
// Map tacit display status to actual resolution filter
if (filters.resolution === 'pending') {
if (row.deadline.resolution !== 'pending') return false;
if (filters.resolution === "pending") {
if (row.deadline.resolution !== "pending") return false;
} else if (row.deadline.resolution !== filters.resolution) {
return false;
}
}
if (filters.urgentOnly) {
if (row.status.variant !== 'yellow' && row.status.variant !== 'red') return false;
if (row.status.variant !== "yellow" && row.status.variant !== "red")
return false;
}
return true;
});
@@ -60,16 +88,31 @@ export function DeadlineDashboard({ entries, onResolveDeadline, onAddChainedDead
setResolvingDeadline(deadline);
};
const handleResolve = (resolution: DeadlineResolution, note: string, chainNext: boolean) => {
const handleResolve = (
resolution: DeadlineResolution,
note: string,
chainNext: boolean,
) => {
if (!resolvingEntry || !resolvingDeadline) return;
onResolveDeadline(resolvingEntry, resolvingDeadline.id, resolution, note, chainNext);
onResolveDeadline(
resolvingEntry,
resolvingDeadline.id,
resolution,
note,
chainNext,
);
// Handle chain creation
if (chainNext) {
const def = getDeadlineType(resolvingDeadline.typeId);
if (def?.chainNextTypeId) {
const resolvedDate = new Date().toISOString().slice(0, 10);
onAddChainedDeadline(resolvingEntry, def.chainNextTypeId, resolvedDate, resolvingDeadline.id);
onAddChainedDeadline(
resolvingEntry,
def.chainNextTypeId,
resolvedDate,
resolvingDeadline.id,
);
}
}
@@ -80,43 +123,104 @@ export function DeadlineDashboard({ entries, onResolveDeadline, onAddChainedDead
return (
<div className="space-y-4">
{/* Stats */}
<div className="grid grid-cols-2 gap-3 sm:grid-cols-4">
<div className="grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-6">
<StatCard label="Active" value={stats.active} />
<StatCard label="Urgente" value={stats.urgent} variant={stats.urgent > 0 ? 'destructive' : undefined} />
<StatCard label="Depășit termen" value={stats.overdue} variant={stats.overdue > 0 ? 'destructive' : undefined} />
<StatCard label="Aprobat tacit" value={stats.tacit} variant={stats.tacit > 0 ? 'blue' : undefined} />
<StatCard
label="Urgente"
value={stats.urgent}
variant={stats.urgent > 0 ? "destructive" : undefined}
/>
<StatCard
label="Depășit termen"
value={stats.overdue}
variant={stats.overdue > 0 ? "destructive" : undefined}
/>
<StatCard
label="Aprobat tacit"
value={stats.tacit}
variant={stats.tacit > 0 ? "blue" : undefined}
/>
<StatCard
label="Lipsă nr. dest."
value={stats.missingRecipientReg}
variant={stats.missingRecipientReg > 0 ? "destructive" : undefined}
/>
<StatCard
label="Expiră curând"
value={stats.expiringSoon}
variant={stats.expiringSoon > 0 ? "destructive" : undefined}
/>
</div>
{/* Alert banners */}
{stats.missingRecipientReg > 0 && (
<div className="rounded-md border border-amber-500/30 bg-amber-500/5 px-4 py-2 text-xs text-amber-700 dark:text-amber-400">
{stats.missingRecipientReg} ieșir
{stats.missingRecipientReg === 1 ? "e" : "i"} cu termene legale nu{" "}
{stats.missingRecipientReg === 1 ? "are" : "au"} completat
numărul/data de înregistrare la destinatar. Termenele legale nu
pornesc fără aceste date.
</div>
)}
{stats.expiringSoon > 0 && (
<div className="rounded-md border border-red-500/30 bg-red-500/5 px-4 py-2 text-xs text-red-700 dark:text-red-400">
{stats.expiringSoon} document{stats.expiringSoon === 1 ? "" : "e"}{" "}
cu termen de valabilitate se apropie de expirare sau{" "}
{stats.expiringSoon === 1 ? "a" : "au"} expirat. Inițiați procedurile
de prelungire.
</div>
)}
{/* Filters */}
<div className="flex flex-wrap items-end gap-3">
<div>
<Label className="text-xs">Categorie</Label>
<Select value={filters.category} onValueChange={(v) => updateFilter('category', v as DeadlineCategory | 'all')}>
<SelectTrigger className="mt-1 w-[160px]"><SelectValue /></SelectTrigger>
<Select
value={filters.category}
onValueChange={(v) =>
updateFilter("category", v as DeadlineCategory | "all")
}
>
<SelectTrigger className="mt-1 w-[160px]">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">Toate</SelectItem>
{(Object.entries(CATEGORY_LABELS) as [DeadlineCategory, string][]).map(([key, label]) => (
<SelectItem key={key} value={key}>{label}</SelectItem>
{(
Object.entries(CATEGORY_LABELS) as [DeadlineCategory, string][]
).map(([key, label]) => (
<SelectItem key={key} value={key}>
{label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div>
<Label className="text-xs">Status</Label>
<Select value={filters.resolution} onValueChange={(v) => updateFilter('resolution', v as DeadlineResolution | 'all')}>
<SelectTrigger className="mt-1 w-[160px]"><SelectValue /></SelectTrigger>
<Select
value={filters.resolution}
onValueChange={(v) =>
updateFilter("resolution", v as DeadlineResolution | "all")
}
>
<SelectTrigger className="mt-1 w-[160px]">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">Toate</SelectItem>
{Object.entries(RESOLUTION_LABELS).map(([key, label]) => (
<SelectItem key={key} value={key}>{label}</SelectItem>
<SelectItem key={key} value={key}>
{label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<Button
variant={filters.urgentOnly ? 'default' : 'outline'}
variant={filters.urgentOnly ? "default" : "outline"}
size="sm"
onClick={() => updateFilter('urgentOnly', !filters.urgentOnly)}
onClick={() => updateFilter("urgentOnly", !filters.urgentOnly)}
>
Doar urgente
</Button>
@@ -144,14 +248,24 @@ export function DeadlineDashboard({ entries, onResolveDeadline, onAddChainedDead
);
}
function StatCard({ label, value, variant }: { label: string; value: number; variant?: 'destructive' | 'blue' }) {
function StatCard({
label,
value,
variant,
}: {
label: string;
value: number;
variant?: "destructive" | "blue";
}) {
return (
<Card>
<CardContent className="p-4">
<p className="text-xs text-muted-foreground">{label}</p>
<p className={`text-2xl font-bold ${
variant === 'destructive' && value > 0 ? 'text-destructive' : ''
}${variant === 'blue' && value > 0 ? 'text-blue-600' : ''}`}>
<p
className={`text-2xl font-bold ${
variant === "destructive" && value > 0 ? "text-destructive" : ""
}${variant === "blue" && value > 0 ? "text-blue-600" : ""}`}
>
{value}
</p>
</CardContent>
@@ -228,6 +228,9 @@ export function RegistraturaModule() {
[allEntries],
);
const urgentDeadlines = deadlineStats.urgent + deadlineStats.overdue;
const missingRecipient = deadlineStats.missingRecipientReg;
const expiringSoon = deadlineStats.expiringSoon;
const alertCount = urgentDeadlines + missingRecipient + expiringSoon;
const closingEntry = closingId
? allEntries.find((e) => e.id === closingId)
@@ -248,12 +251,12 @@ export function RegistraturaModule() {
<TabsTrigger value="registru">Registru</TabsTrigger>
<TabsTrigger value="termene">
Termene legale
{urgentDeadlines > 0 && (
{alertCount > 0 && (
<Badge
variant="destructive"
className="ml-1.5 text-[10px] px-1.5 py-0"
>
{urgentDeadlines}
{alertCount}
</Badge>
)}
</TabsTrigger>
@@ -261,6 +264,21 @@ export function RegistraturaModule() {
<TabsContent value="registru">
<div className="space-y-6">
{/* Alert banners */}
{missingRecipient > 0 && viewMode === "list" && (
<div className="rounded-md border border-amber-500/30 bg-amber-500/5 px-4 py-2 text-xs text-amber-700 dark:text-amber-400">
{missingRecipient} ieșir{missingRecipient === 1 ? "e" : "i"} cu
termene legale nu {missingRecipient === 1 ? "are" : "au"}{" "}
completat datele de la destinatar.
</div>
)}
{expiringSoon > 0 && viewMode === "list" && (
<div className="rounded-md border border-red-500/30 bg-red-500/5 px-4 py-2 text-xs text-red-700 dark:text-red-400">
{expiringSoon} document{expiringSoon === 1 ? "" : "e"} se
apropie de expirare sau {expiringSoon === 1 ? "a" : "au"} expirat.
</div>
)}
{/* Stats */}
<div className="grid grid-cols-2 gap-3 sm:grid-cols-4">
<StatCard label="Total" value={total} />
@@ -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>