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:
@@ -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
|
||||
"Intrerupt" — 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);
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user