feat: avize deadline restructure with interruption mechanism + comisie toggle

- Add "Necesita analiza in comisie" toggle for avize category (mirrors CJ toggle)
  - When OFF: auto-creates 5-day working limit for completari requests
  - When ON: no limit (institution can request completions anytime)
- Add interruption mechanism: resolve aviz as "intrerupt" when institution
  requests completions → auto-creates new 15-day deadline from completions date
- New resolution type "intrerupt" with yellow badge + chain support
- Restructure avize catalog entries:
  - aviz-ac-15 (L50) and aviz-urbanism-30 (L350) now have chain to
    aviz-emitere-dupa-completari for interruption flow
  - aviz-mediu: updated hints about procedure closure prerequisite
  - aviz-cultura-comisie: 2-phase with auto-track depunere la comisie (30 days)
  - aeronautica, ISU, transport-eu: all get interruption chain
- 3 new auto-track entries: aviz-completari-limit (5zl), aviz-emitere-dupa-completari
  (15zc), aviz-cultura-depunere-comisie (30zc)
- New document type: "Convocare sedinta"
- Info boxes in dialog explaining auto-track behavior + interruption mechanism

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
AI Assistant
2026-03-10 22:41:14 +02:00
parent 31565b418a
commit 442a1565fd
7 changed files with 252 additions and 31 deletions
@@ -32,11 +32,11 @@ interface DeadlineAddDialogProps {
direction: RegistryDirection;
/** Document type — filters which deadline categories are available */
documentType?: string;
/** Callback: typeId, startDate, options (CJ toggle etc.) */
/** Callback: typeId, startDate, options (CJ / Comisie toggles etc.) */
onAdd: (
typeId: string,
startDate: string,
options?: { isCJ?: boolean },
options?: { isCJ?: boolean; isComisie?: boolean },
) => void;
}
@@ -58,6 +58,7 @@ export function DeadlineAddDialog({
);
const [startDate, setStartDate] = useState(entryDate);
const [isCJ, setIsCJ] = useState(false);
const [isComisie, setIsComisie] = useState(false);
// ── Prelungire helper state ──
const [cuIssueDate, setCuIssueDate] = useState("");
@@ -104,6 +105,7 @@ export function DeadlineAddDialog({
setSelectedType(null);
setStartDate(entryDate);
setIsCJ(false);
setIsComisie(false);
setCuIssueDate("");
setCuDurationMonths(null);
onOpenChange(false);
@@ -127,6 +129,7 @@ export function DeadlineAddDialog({
setStep("category");
setSelectedCategory(null);
setIsCJ(false);
setIsComisie(false);
} else if (step === "date") {
setStep("type");
setSelectedType(null);
@@ -140,8 +143,10 @@ export function DeadlineAddDialog({
const isCUType =
selectedType.id === "cu-emitere-l50" ||
selectedType.id === "cu-emitere-l350";
const isAvizeType = selectedCategory === "avize";
onAdd(selectedType.id, startDate, {
isCJ: isCUType ? isCJ : undefined,
isComisie: isAvizeType ? isComisie : undefined,
});
handleClose();
};
@@ -238,6 +243,30 @@ export function DeadlineAddDialog({
</label>
)}
{/* Comisie toggle for Avize category */}
{selectedCategory === "avize" && (
<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={isComisie}
onChange={(e) => setIsComisie(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">
Necesita analiza in comisie
</span>
<p className="text-[10px] text-muted-foreground">
Cand e activ: institutia poate solicita completari oricand
inainte de analiza in comisie. Cand e dezactivat: se creeaza
automat limita de 5 zile lucratoare pentru solicitare
completari.
</p>
</div>
</label>
)}
<div className="grid gap-2 max-h-[400px] overflow-y-auto">
{typesForCategory.map((typ) => (
<button
@@ -300,6 +329,31 @@ export function DeadlineAddDialog({
</div>
</div>
)}
{/* Info about auto-tracked deadlines for Avize */}
{selectedCategory === "avize" && (
<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>
{!isComisie && (
<p className="mt-0.5">
Limita solicitare completari (5 zile lucr.) dupa acest
termen, institutia nu mai poate solicita completari.
</p>
)}
<p className="mt-0.5">
Comunicare catre beneficiar (1 zi) obligatia institutiei
de a transmite avizul in ziua emiterii.
</p>
<p className="mt-1 text-[10px] opacity-75">
Daca institutia solicita completari, rezolvati termenul ca
&quot;Intrerupt&quot; se va crea automat termen nou de 15
zile de la depunerea completarilor.
</p>
</div>
</div>
)}
</div>
)}
@@ -109,6 +109,14 @@ export function DeadlineCard({
</Badge>
</div>
)}
{/* Interruption badge */}
{deadline.resolution === "intrerupt" && (
<div className="flex items-center gap-1 mt-1">
<Badge className="text-[10px] border-0 bg-amber-100 text-amber-800 dark:bg-amber-900 dark:text-amber-200">
Intrerupt completari solicitate
</Badge>
</div>
)}
<div className="text-xs text-muted-foreground mt-0.5">
{def?.isBackwardDeadline ? "Termen limita" : "Start"}:{" "}
{formatDate(deadline.startDate)}
@@ -1,6 +1,6 @@
'use client';
import { useState } from 'react';
import { useState, useMemo } from 'react';
import {
Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter,
} from '@/shared/components/ui/dialog';
@@ -18,9 +18,10 @@ interface DeadlineResolveDialogProps {
onResolve: (resolution: DeadlineResolution, note: string, chainNext: boolean) => void;
}
const RESOLUTION_OPTIONS: Array<{ value: DeadlineResolution; label: string }> = [
const ALL_RESOLUTION_OPTIONS: Array<{ value: DeadlineResolution; label: string }> = [
{ value: 'completed', label: 'Finalizat' },
{ value: 'aprobat-tacit', label: 'Aprobat tacit' },
{ value: 'intrerupt', label: 'Intrerupt (completari solicitate)' },
{ value: 'respins', label: 'Respins' },
{ value: 'anulat', label: 'Anulat' },
];
@@ -32,7 +33,22 @@ export function DeadlineResolveDialog({ open, deadline, onOpenChange, onResolve
if (!deadline) return null;
const def = getDeadlineType(deadline.typeId);
const hasChain = def?.chainNextTypeId && (resolution === 'completed' || resolution === 'aprobat-tacit');
// "intrerupt" option only for avize deadlines that have a chain (interruption mechanism)
const filteredOptions = useMemo(() => {
return ALL_RESOLUTION_OPTIONS.filter((opt) => {
if (opt.value === 'intrerupt') {
return def?.chainNextTypeId && def?.category === 'avize';
}
return true;
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [def?.chainNextTypeId, def?.category]);
// Chain fires on completed, aprobat-tacit, or intrerupt (for avize interruption)
const hasChain = def?.chainNextTypeId && (
resolution === 'completed' || resolution === 'aprobat-tacit' || resolution === 'intrerupt'
);
const handleResolve = () => {
onResolve(resolution, note, !!hasChain);
@@ -51,16 +67,16 @@ export function DeadlineResolveDialog({ open, deadline, onOpenChange, onResolve
<Dialog open={open} onOpenChange={(o) => { if (!o) handleClose(); }}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>Rezolvă {def?.label ?? deadline.typeId}</DialogTitle>
<DialogTitle>Rezolva {def?.label ?? deadline.typeId}</DialogTitle>
</DialogHeader>
<div className="space-y-4 py-2">
<div>
<Label>Rezoluție</Label>
<Label>Rezolutie</Label>
<Select value={resolution} onValueChange={(v) => setResolution(v as DeadlineResolution)}>
<SelectTrigger className="mt-1"><SelectValue /></SelectTrigger>
<SelectContent>
{RESOLUTION_OPTIONS.map((opt) => (
{filteredOptions.map((opt) => (
<SelectItem key={opt.value} value={opt.value}>{opt.label}</SelectItem>
))}
</SelectContent>
@@ -68,31 +84,44 @@ export function DeadlineResolveDialog({ open, deadline, onOpenChange, onResolve
</div>
<div>
<Label>Notă (opțional)</Label>
<Label>Nota (optional)</Label>
<Textarea
value={note}
onChange={(e) => setNote(e.target.value)}
rows={2}
className="mt-1"
placeholder="Detalii rezoluție..."
placeholder="Detalii rezolutie..."
/>
</div>
{hasChain && def?.chainNextActionLabel && (
{hasChain && resolution !== 'intrerupt' && def?.chainNextActionLabel && (
<div className="rounded-lg border border-blue-500/30 bg-blue-50 dark:bg-blue-950/20 p-3">
<p className="text-sm font-medium text-blue-800 dark:text-blue-200">
Termen înlănțuit disponibil
Termen inlantuit disponibil
</p>
<p className="text-xs text-blue-700 dark:text-blue-300 mt-1">
La rezolvare veți fi întrebat dacă doriți: {def.chainNextActionLabel}
La rezolvare se va crea automat: {def.chainNextActionLabel}
</p>
</div>
)}
{resolution === 'intrerupt' && def?.chainNextTypeId && (
<div className="rounded-lg border border-amber-500/30 bg-amber-50 dark:bg-amber-950/20 p-3">
<p className="text-sm font-medium text-amber-800 dark:text-amber-200">
Termen intrerupt completari solicitate
</p>
<p className="text-xs text-amber-700 dark:text-amber-300 mt-1">
Termenul initial se intrerupe. Se va crea automat un nou termen de 15 zile
calendaristice care incepe de la data depunerii completarilor. Actualizati
data start cand completarile sunt depuse.
</p>
</div>
)}
</div>
<DialogFooter>
<Button type="button" variant="outline" onClick={handleClose}>Anulează</Button>
<Button type="button" onClick={handleResolve}>Rezolvă</Button>
<Button type="button" variant="outline" onClick={handleClose}>Anuleaza</Button>
<Button type="button" onClick={handleResolve}>Rezolva</Button>
</DialogFooter>
</DialogContent>
</Dialog>
@@ -256,7 +256,7 @@ export function RegistryEntryForm({
const handleAddDeadline = (
typeId: string,
startDate: string,
options?: { isCJ?: boolean; chainParentId?: string },
options?: { isCJ?: boolean; isComisie?: boolean; chainParentId?: string },
) => {
const tracked = createTrackedDeadline(
typeId,
@@ -300,6 +300,25 @@ export function RegistryEntryForm({
}
}
// Auto-create completari limit for avize (when Comisie toggle is OFF)
const addedDef = getDeadlineType(typeId);
if (addedDef?.category === "avize" && !options?.isComisie) {
const completariLimit = createTrackedDeadline(
"aviz-completari-limit",
startDate,
);
if (completariLimit) newDeadlines.push(completariLimit);
}
// Auto-create Cultura Phase 1 (depunere la comisie) when adding cultura-comisie
if (typeId === "aviz-cultura-comisie") {
const culturaPhase1 = createTrackedDeadline(
"aviz-cultura-depunere-comisie",
startDate,
);
if (culturaPhase1) newDeadlines.push(culturaPhase1);
}
// Auto-create comunicare catre beneficiar for all iesit deadlines
// (legal obligation: institution must communicate on the day of issuance)
if (direction === "iesit") {
@@ -325,7 +344,8 @@ export function RegistryEntryForm({
prev.map((d) => (d.id === resolved.id ? resolved : d)),
);
if (chainNext) {
// Standard chain (completed / aprobat-tacit)
if (chainNext && resolution !== "intrerupt") {
const def = getDeadlineType(resolvingDeadline.typeId);
if (def?.chainNextTypeId) {
const resolvedDate = new Date().toISOString().slice(0, 10);
@@ -337,6 +357,34 @@ export function RegistryEntryForm({
if (chained) setTrackedDeadlines((prev) => [...prev, chained]);
}
}
// Interruption chain — institution requested completions, term interrupted
// Creates chained deadline with today as placeholder start date
// User must update start date when completions are actually submitted
if (resolution === "intrerupt") {
const def = getDeadlineType(resolvingDeadline.typeId);
if (def?.chainNextTypeId) {
const today = new Date().toISOString().slice(0, 10);
const chained = createTrackedDeadline(
def.chainNextTypeId,
today,
resolvingDeadline.id,
);
if (chained) {
chained.auditLog = [
...(chained.auditLog ?? []),
{
action: "modified",
timestamp: new Date().toISOString(),
detail:
"ATENTIE: Actualizati data start cand se depun completarile/clarificarile",
},
];
setTrackedDeadlines((prev) => [...prev, chained]);
}
}
}
setResolvingDeadline(null);
};