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>