feat(registratura): redesign CU deadline tracking — direction filtering, CJ toggle, auto-track, verification badge

- CU has NO tacit approval on any entry
- Direction-dependent categories: iesiri (CU, Avize, Completari, Urbanism, Autorizare), intrari (Contestatie)
- Rename: Analiza → Urbanism (PUD/PUZ/PUG), Autorizare (AC) → Autorizare (AD/AC)
- Auto-track deadlines: cu-verificare (10zl) created automatically with CU emitere
- CJ toggle: auto-creates arhitect-sef solicita aviz (3zc) + primar emite aviz (5zc)
- Verification badge: after 10 days shows "Nu mai pot returna documentatia"
- Prelungire helper: CU issue date + 6/12/24 month calculator
- cu-prelungire-emitere changed to 30zc (practica administrativa)
- New DeadlineTypeDef fields: autoTrack, directionFilter

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
AI Assistant
2026-03-10 18:58:50 +02:00
parent f5ffce2e23
commit b2519a3b9c
5 changed files with 537 additions and 147 deletions
@@ -12,36 +12,39 @@ import { Button } from "@/shared/components/ui/button";
import { Input } from "@/shared/components/ui/input";
import { Label } from "@/shared/components/ui/label";
import { Badge } from "@/shared/components/ui/badge";
import { Info, Building2, Calendar } from "lucide-react";
import {
DEADLINE_CATALOG,
CATEGORY_LABELS,
getCategoriesForDirection,
getSelectableDeadlines,
} from "../services/deadline-catalog";
import { computeDueDate } from "../services/working-days";
import type { DeadlineCategory, DeadlineTypeDef } from "../types";
import type {
DeadlineCategory,
DeadlineTypeDef,
RegistryDirection,
} from "../types";
interface DeadlineAddDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
entryDate: string;
onAdd: (typeId: string, startDate: string) => void;
direction: RegistryDirection;
/** Callback: typeId, startDate, options (CJ toggle etc.) */
onAdd: (
typeId: string,
startDate: string,
options?: { isCJ?: boolean },
) => void;
}
type Step = "category" | "type" | "date";
const CATEGORIES: DeadlineCategory[] = [
"certificat",
"avize",
"completari",
"analiza",
"autorizare",
"publicitate",
"contestatie",
];
export function DeadlineAddDialog({
open,
onOpenChange,
entryDate,
direction,
onAdd,
}: DeadlineAddDialogProps) {
const [step, setStep] = useState<Step>("category");
@@ -51,10 +54,20 @@ export function DeadlineAddDialog({
null,
);
const [startDate, setStartDate] = useState(entryDate);
const [isCJ, setIsCJ] = useState(false);
// ── Prelungire helper state ──
const [cuIssueDate, setCuIssueDate] = useState("");
const [cuDurationMonths, setCuDurationMonths] = useState<number | null>(null);
const categories = useMemo(
() => getCategoriesForDirection(direction),
[direction],
);
const typesForCategory = useMemo(() => {
if (!selectedCategory) return [];
return DEADLINE_CATALOG.filter((d) => d.category === selectedCategory);
return getSelectableDeadlines(selectedCategory);
}, [selectedCategory]);
const dueDatePreview = useMemo(() => {
@@ -74,11 +87,22 @@ export function DeadlineAddDialog({
});
}, [selectedType, startDate]);
// Compute CU expiry when user uses the prelungire helper
const computedExpiryDate = useMemo(() => {
if (!cuIssueDate || !cuDurationMonths) return null;
const issue = new Date(cuIssueDate);
issue.setMonth(issue.getMonth() + cuDurationMonths);
return issue;
}, [cuIssueDate, cuDurationMonths]);
const handleClose = () => {
setStep("category");
setSelectedCategory(null);
setSelectedType(null);
setStartDate(entryDate);
setIsCJ(false);
setCuIssueDate("");
setCuDurationMonths(null);
onOpenChange(false);
};
@@ -99,18 +123,41 @@ export function DeadlineAddDialog({
if (step === "type") {
setStep("category");
setSelectedCategory(null);
setIsCJ(false);
} else if (step === "date") {
setStep("type");
setSelectedType(null);
setCuIssueDate("");
setCuDurationMonths(null);
}
};
const handleConfirm = () => {
if (!selectedType || !startDate) return;
onAdd(selectedType.id, startDate);
const isCUType =
selectedType.id === "cu-emitere-l50" ||
selectedType.id === "cu-emitere-l350";
onAdd(selectedType.id, startDate, {
isCJ: isCUType ? isCJ : undefined,
});
handleClose();
};
// Apply prelungire helper: set start date from computed expiry
const handleApplyExpiryHelper = () => {
if (computedExpiryDate) {
const y = computedExpiryDate.getFullYear();
const m = String(computedExpiryDate.getMonth() + 1).padStart(2, "0");
const d = String(computedExpiryDate.getDate()).padStart(2, "0");
setStartDate(`${y}-${m}-${d}`);
}
};
const isPrelungireType = selectedType?.id === "cu-prelungire-emitere";
const isCUEmitere =
selectedType?.id === "cu-emitere-l50" ||
selectedType?.id === "cu-emitere-l350";
return (
<Dialog
open={open}
@@ -121,81 +168,208 @@ export function DeadlineAddDialog({
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>
{step === "category" && "Adaugă termen legal — Categorie"}
{step === "category" && "Adauga termen legal — Categorie"}
{step === "type" &&
`Adaugă termen legal — ${selectedCategory ? CATEGORY_LABELS[selectedCategory] : ""}`}
`Adauga termen legal — ${selectedCategory ? CATEGORY_LABELS[selectedCategory] : ""}`}
{step === "date" &&
`Adaugă termen legal — ${selectedType?.label ?? ""}`}
`Adauga termen legal — ${selectedType?.label ?? ""}`}
</DialogTitle>
</DialogHeader>
{/* ── Step 1: Category selection ── */}
{step === "category" && (
<div className="grid gap-2 py-2">
{CATEGORIES.map((cat) => (
<button
key={cat}
type="button"
className="flex items-center justify-between rounded-lg border p-3 text-left transition-colors hover:bg-accent"
onClick={() => handleCategorySelect(cat)}
>
<span className="font-medium text-sm">
{CATEGORY_LABELS[cat]}
</span>
<Badge variant="outline" className="text-xs">
{DEADLINE_CATALOG.filter((d) => d.category === cat).length}
</Badge>
</button>
))}
{categories.map((cat) => {
const count = getSelectableDeadlines(cat).length;
return (
<button
key={cat}
type="button"
className="flex items-center justify-between rounded-lg border p-3 text-left transition-colors hover:bg-accent"
onClick={() => handleCategorySelect(cat)}
>
<span className="font-medium text-sm">
{CATEGORY_LABELS[cat]}
</span>
<Badge variant="outline" className="text-xs">
{count}
</Badge>
</button>
);
})}
{/* Direction info */}
<div className="flex items-start gap-2 mt-2 rounded-lg bg-muted/50 p-2.5">
<Info className="h-3.5 w-3.5 mt-0.5 text-muted-foreground shrink-0" />
<p className="text-[11px] text-muted-foreground">
{direction === "iesit"
? "Categoriile afisate sunt pentru demersuri depuse de noi (iesiri) — termene pe care le urmarim la institutii."
: "Categoriile afisate sunt pentru acte administrative primite (intrari) — termene de contestare/raspuns."}
</p>
</div>
</div>
)}
{/* ── Step 2: Type selection ── */}
{step === "type" && (
<div className="grid gap-2 py-2 max-h-[400px] overflow-y-auto">
{typesForCategory.map((typ) => (
<button
key={typ.id}
type="button"
className="rounded-lg border p-3 text-left transition-colors hover:bg-accent"
onClick={() => handleTypeSelect(typ)}
>
<div className="flex items-center gap-2">
<span className="font-medium text-sm">{typ.label}</span>
<Badge variant="outline" className="text-[10px]">
{typ.days}{" "}
{typ.dayType === "working" ? "zile lucr." : "zile cal."}
</Badge>
{typ.tacitApprovalApplicable && (
<Badge
variant="outline"
className="text-[10px] text-blue-600"
>
tacit
<div className="space-y-2 py-2">
{/* CJ toggle for CU category */}
{selectedCategory === "certificat" && (
<label className="flex items-center gap-2 rounded-lg border border-dashed p-2.5 cursor-pointer hover:bg-accent/50 transition-colors">
<input
type="checkbox"
checked={isCJ}
onChange={(e) => setIsCJ(e.target.checked)}
className="h-4 w-4 rounded border-gray-300"
/>
<Building2 className="h-4 w-4 text-muted-foreground" />
<div>
<span className="text-sm font-medium">
Solicitat la Consiliul Judetean
</span>
<p className="text-[10px] text-muted-foreground">
Activeaza sub-termene automate: arhitect-sef solicita aviz
primar (3z) + primar emite aviz (5z)
</p>
</div>
</label>
)}
<div className="grid gap-2 max-h-[400px] overflow-y-auto">
{typesForCategory.map((typ) => (
<button
key={typ.id}
type="button"
className="rounded-lg border p-3 text-left transition-colors hover:bg-accent"
onClick={() => handleTypeSelect(typ)}
>
<div className="flex items-center gap-2 flex-wrap">
<span className="font-medium text-sm">{typ.label}</span>
<Badge variant="outline" className="text-[10px]">
{typ.days}{" "}
{typ.dayType === "working" ? "zile lucr." : "zile cal."}
</Badge>
{typ.tacitApprovalApplicable && (
<Badge
variant="outline"
className="text-[10px] text-blue-600"
>
tacit
</Badge>
)}
{typ.isBackwardDeadline && (
<Badge
variant="outline"
className="text-[10px] text-orange-600"
>
inapoi
</Badge>
)}
</div>
<p className="text-xs text-muted-foreground mt-1">
{typ.description}
</p>
{typ.legalReference && (
<p className="text-[10px] text-muted-foreground/70 mt-0.5 italic">
{typ.legalReference}
</p>
)}
{typ.isBackwardDeadline && (
<Badge
variant="outline"
className="text-[10px] text-orange-600"
>
înapoi
</Badge>
</button>
))}
</div>
{/* Info about auto-tracked deadlines for CU */}
{selectedCategory === "certificat" && (
<div className="flex items-start gap-2 rounded-lg bg-blue-50 dark:bg-blue-950/20 border border-blue-200 dark:border-blue-800 p-2.5">
<Info className="h-3.5 w-3.5 mt-0.5 text-blue-600 shrink-0" />
<div className="text-[11px] text-blue-800 dark:text-blue-300">
<p className="font-medium">Termene automate (in fundal):</p>
<p className="mt-0.5">
Verificare cerere CU (10 zile lucr.) se creeaza automat.
Dupa expirare, institutia nu mai poate returna documentatia.
</p>
{isCJ && (
<p className="mt-0.5">
Aviz primar CJ (3z + 5z) se creeaza automat la
bifarea optiunii CJ.
</p>
)}
</div>
<p className="text-xs text-muted-foreground mt-1">
{typ.description}
</p>
{typ.legalReference && (
<p className="text-[10px] text-muted-foreground/70 mt-0.5 italic">
{typ.legalReference}
</p>
)}
</button>
))}
</div>
)}
</div>
)}
{/* ── Step 3: Date confirmation + preview ── */}
{step === "date" && selectedType && (
<div className="space-y-4 py-2">
{/* Prelungire helper: compute expiry from CU issue date */}
{isPrelungireType && (
<div className="rounded-lg border border-dashed p-3 space-y-2">
<div className="flex items-center gap-1.5">
<Calendar className="h-3.5 w-3.5 text-muted-foreground" />
<span className="text-xs font-medium">
Calculator data expirare CU
</span>
</div>
<p className="text-[10px] text-muted-foreground">
Introdu data emiterii CU si selecteaza durata de valabilitate
pentru a calcula automat data expirarii.
</p>
<div className="flex gap-2 items-end">
<div className="flex-1">
<Label className="text-[10px]">Data emitere CU</Label>
<Input
type="date"
value={cuIssueDate}
onChange={(e) => setCuIssueDate(e.target.value)}
className="mt-0.5 h-8 text-xs"
/>
</div>
<div className="flex gap-1">
{[6, 12, 24].map((months) => (
<Button
key={months}
type="button"
variant={
cuDurationMonths === months ? "default" : "outline"
}
size="sm"
className="h-8 text-xs px-2"
onClick={() => setCuDurationMonths(months)}
>
{months} luni
</Button>
))}
</div>
</div>
{computedExpiryDate && (
<div className="flex items-center justify-between rounded bg-muted/50 px-2 py-1.5">
<span className="text-xs text-muted-foreground">
Data expirare CU:{" "}
<strong>
{computedExpiryDate.toLocaleDateString("ro-RO", {
day: "2-digit",
month: "2-digit",
year: "numeric",
})}
</strong>
</span>
<Button
type="button"
variant="outline"
size="sm"
className="h-6 text-[10px] px-2"
onClick={handleApplyExpiryHelper}
>
Aplica ca data start
</Button>
</div>
)}
</div>
)}
{/* Start date input */}
<div>
<Label>{selectedType.startDateLabel}</Label>
{selectedType.startDateHint && (
@@ -211,21 +385,22 @@ export function DeadlineAddDialog({
/>
</div>
{/* Due date preview */}
{dueDatePreview && (
<div className="rounded-lg border bg-muted/30 p-3">
<p className="text-xs text-muted-foreground">
{selectedType.isBackwardDeadline
? "Termen limită depunere"
: "Termen limită calculat"}
? "Termen limita depunere"
: "Termen limita calculat"}
</p>
<p className="text-lg font-bold">{dueDatePreview}</p>
<p className="text-xs text-muted-foreground mt-1">
{selectedType.days}{" "}
{selectedType.dayType === "working"
? "zile lucrătoare"
? "zile lucratoare"
: "zile calendaristice"}
{selectedType.isBackwardDeadline
? " ÎNAINTE"
? " INAINTE"
: " de la data start"}
</p>
{selectedType.legalReference && (
@@ -235,23 +410,38 @@ export function DeadlineAddDialog({
)}
</div>
)}
{/* CJ info reminder */}
{isCUEmitere && isCJ && (
<div className="flex items-start gap-2 rounded-lg bg-amber-50 dark:bg-amber-950/20 border border-amber-200 dark:border-amber-800 p-2.5">
<Building2 className="h-3.5 w-3.5 mt-0.5 text-amber-600 shrink-0" />
<p className="text-[11px] text-amber-800 dark:text-amber-300">
Solicitat la CJ se vor crea automat sub-termenele de aviz
primar (3z solicitat + 5z emis).
</p>
</div>
)}
</div>
)}
<DialogFooter className="gap-2 sm:gap-0">
{step !== "category" && (
<Button type="button" variant="outline" onClick={handleBack}>
Înapoi
Inapoi
</Button>
)}
{step === "category" && (
<Button type="button" variant="outline" onClick={handleClose}>
Anulează
Anuleaza
</Button>
)}
{step === "date" && (
<Button type="button" onClick={handleConfirm} disabled={!startDate}>
Adaugă termen
<Button
type="button"
onClick={handleConfirm}
disabled={!startDate}
>
Adauga termen
</Button>
)}
</DialogFooter>
@@ -1,7 +1,7 @@
"use client";
import { useState } from "react";
import { Clock, CheckCircle2, X, History } from "lucide-react";
import { Clock, CheckCircle2, X, History, ShieldCheck, Building2 } from "lucide-react";
import { Badge } from "@/shared/components/ui/badge";
import { Button } from "@/shared/components/ui/button";
import type { TrackedDeadline } from "../types";
@@ -42,6 +42,19 @@ export function DeadlineCard({
const [showAudit, setShowAudit] = useState(false);
const auditLog = deadline.auditLog ?? [];
const isAutoTrack = def?.autoTrack === true;
const isVerificationDeadline = deadline.typeId === "cu-verificare";
const isCJDeadline =
deadline.typeId === "cu-cj-solicitare-aviz" ||
deadline.typeId === "cu-cj-aviz-primar";
// For verification deadlines, check if the 10-day period has passed
const verificationExpired =
isVerificationDeadline &&
deadline.resolution === "pending" &&
status.daysRemaining !== null &&
status.daysRemaining < 0;
return (
<div
className={cn(
@@ -50,12 +63,25 @@ export function DeadlineCard({
)}
>
<div className="flex items-center gap-3">
<Clock className="h-4 w-4 shrink-0 text-muted-foreground" />
{isAutoTrack ? (
isCJDeadline ? (
<Building2 className="h-4 w-4 shrink-0 text-amber-500" />
) : (
<ShieldCheck className="h-4 w-4 shrink-0 text-blue-500" />
)
) : (
<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">
<div className="flex items-center gap-2 flex-wrap">
<span className="text-sm font-medium truncate">
{def?.label ?? deadline.typeId}
</span>
{isAutoTrack && (
<Badge variant="outline" className="text-[10px] text-purple-600 border-purple-300">
auto
</Badge>
)}
<Badge
className={cn(
"text-[10px] border-0",
@@ -67,21 +93,30 @@ export function DeadlineCard({
<span className="ml-1">
(
{status.daysRemaining < 0
? `${Math.abs(status.daysRemaining)}z depășit`
? `${Math.abs(status.daysRemaining)}z depasit`
: `${status.daysRemaining}z`}
)
</span>
)}
</Badge>
</div>
{/* Verification badge: institution lost right to return docs */}
{verificationExpired && (
<div className="flex items-center gap-1 mt-1">
<Badge className="text-[10px] border-0 bg-emerald-100 text-emerald-800 dark:bg-emerald-900 dark:text-emerald-200">
<ShieldCheck className="h-3 w-3 mr-0.5" />
Nu mai pot returna documentatia
</Badge>
</div>
)}
<div className="text-xs text-muted-foreground mt-0.5">
{def?.isBackwardDeadline ? "Termen limită" : "Start"}:{" "}
{def?.isBackwardDeadline ? "Termen limita" : "Start"}:{" "}
{formatDate(deadline.startDate)}
{" "}
{def?.isBackwardDeadline ? "Depunere până la" : "Termen"}:{" "}
{" \u2192 "}
{def?.isBackwardDeadline ? "Depunere pana la" : "Termen"}:{" "}
{formatDate(deadline.dueDate)}
{def?.dayType === "working" && (
<span className="ml-1">(zile lucrătoare)</span>
<span className="ml-1">(zile lucratoare)</span>
)}
</div>
</div>
@@ -238,10 +238,52 @@ export function RegistryEntryForm({
const handleAddDeadline = (
typeId: string,
startDate: string,
chainParentId?: string,
options?: { isCJ?: boolean; chainParentId?: string },
) => {
const tracked = createTrackedDeadline(typeId, startDate, chainParentId);
if (tracked) setTrackedDeadlines((prev) => [...prev, tracked]);
const tracked = createTrackedDeadline(
typeId,
startDate,
options?.chainParentId,
);
if (tracked) {
const newDeadlines = [tracked];
// Auto-create verification deadline for CU emitere types
const isCUEmitere =
typeId === "cu-emitere-l50" || typeId === "cu-emitere-l350";
if (isCUEmitere) {
const verification = createTrackedDeadline(
"cu-verificare",
startDate,
);
if (verification) newDeadlines.push(verification);
}
// Auto-create CJ sub-deadlines if CJ toggle is on
if (isCUEmitere && options?.isCJ) {
const cjRequest = createTrackedDeadline(
"cu-cj-solicitare-aviz",
startDate,
);
if (cjRequest) {
newDeadlines.push(cjRequest);
// Chain: primar responds 5 days from when arhitect-sef requests (3 days from start)
const requestDue = new Date(startDate);
requestDue.setDate(requestDue.getDate() + 3);
const y = requestDue.getFullYear();
const m = String(requestDue.getMonth() + 1).padStart(2, "0");
const d = String(requestDue.getDate()).padStart(2, "0");
const cjResponse = createTrackedDeadline(
"cu-cj-aviz-primar",
`${y}-${m}-${d}`,
cjRequest.id,
);
if (cjResponse) newDeadlines.push(cjResponse);
}
}
setTrackedDeadlines((prev) => [...prev, ...newDeadlines]);
}
};
const handleResolveDeadline = (
@@ -1394,6 +1436,7 @@ export function RegistryEntryForm({
open={deadlineAddOpen}
onOpenChange={setDeadlineAddOpen}
entryDate={date}
direction={direction}
onAdd={handleAddDeadline}
/>