31565b418a
- Fix doc type showing "altele" after edit: preserve initial documentType in allDocTypes map even if not in defaults or Tag Manager - Filter deadline categories by document type: only cerere/aviz unlock full permitting categories (CU, avize, urbanism, autorizare) - Other doc types (scrisoare, notificare, etc.) only get completari + contestatie as deadline categories - Add completari to intrat direction (was missing) - Pass documentType to DeadlineAddDialog for category filtering Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
455 lines
17 KiB
TypeScript
455 lines
17 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useMemo } from "react";
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
DialogFooter,
|
|
} from "@/shared/components/ui/dialog";
|
|
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 {
|
|
CATEGORY_LABELS,
|
|
getCategoriesForDirection,
|
|
getSelectableDeadlines,
|
|
} from "../services/deadline-catalog";
|
|
import { computeDueDate } from "../services/working-days";
|
|
import type {
|
|
DeadlineCategory,
|
|
DeadlineTypeDef,
|
|
RegistryDirection,
|
|
} from "../types";
|
|
|
|
interface DeadlineAddDialogProps {
|
|
open: boolean;
|
|
onOpenChange: (open: boolean) => void;
|
|
entryDate: string;
|
|
direction: RegistryDirection;
|
|
/** Document type — filters which deadline categories are available */
|
|
documentType?: string;
|
|
/** Callback: typeId, startDate, options (CJ toggle etc.) */
|
|
onAdd: (
|
|
typeId: string,
|
|
startDate: string,
|
|
options?: { isCJ?: boolean },
|
|
) => void;
|
|
}
|
|
|
|
type Step = "category" | "type" | "date";
|
|
|
|
export function DeadlineAddDialog({
|
|
open,
|
|
onOpenChange,
|
|
entryDate,
|
|
direction,
|
|
documentType,
|
|
onAdd,
|
|
}: DeadlineAddDialogProps) {
|
|
const [step, setStep] = useState<Step>("category");
|
|
const [selectedCategory, setSelectedCategory] =
|
|
useState<DeadlineCategory | null>(null);
|
|
const [selectedType, setSelectedType] = useState<DeadlineTypeDef | null>(
|
|
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, documentType),
|
|
[direction, documentType],
|
|
);
|
|
|
|
const typesForCategory = useMemo(() => {
|
|
if (!selectedCategory) return [];
|
|
return getSelectableDeadlines(selectedCategory);
|
|
}, [selectedCategory]);
|
|
|
|
const dueDatePreview = useMemo(() => {
|
|
if (!selectedType || !startDate) return null;
|
|
const start = new Date(startDate);
|
|
start.setHours(0, 0, 0, 0);
|
|
const due = computeDueDate(
|
|
start,
|
|
selectedType.days,
|
|
selectedType.dayType,
|
|
selectedType.isBackwardDeadline,
|
|
);
|
|
return due.toLocaleDateString("ro-RO", {
|
|
day: "2-digit",
|
|
month: "2-digit",
|
|
year: "numeric",
|
|
});
|
|
}, [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);
|
|
};
|
|
|
|
const handleCategorySelect = (cat: DeadlineCategory) => {
|
|
setSelectedCategory(cat);
|
|
setStep("type");
|
|
};
|
|
|
|
const handleTypeSelect = (typ: DeadlineTypeDef) => {
|
|
setSelectedType(typ);
|
|
if (!typ.requiresCustomStartDate) {
|
|
setStartDate(entryDate);
|
|
}
|
|
setStep("date");
|
|
};
|
|
|
|
const handleBack = () => {
|
|
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;
|
|
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}
|
|
onOpenChange={(o) => {
|
|
if (!o) handleClose();
|
|
}}
|
|
>
|
|
<DialogContent className="sm:max-w-md">
|
|
<DialogHeader>
|
|
<DialogTitle>
|
|
{step === "category" && "Adauga termen legal — Categorie"}
|
|
{step === "type" &&
|
|
`Adauga termen legal — ${selectedCategory ? CATEGORY_LABELS[selectedCategory] : ""}`}
|
|
{step === "date" &&
|
|
`Adauga termen legal — ${selectedType?.label ?? ""}`}
|
|
</DialogTitle>
|
|
</DialogHeader>
|
|
|
|
{/* ── Step 1: Category selection ── */}
|
|
{step === "category" && (
|
|
<div className="grid gap-2 py-2">
|
|
{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="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>
|
|
)}
|
|
</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>
|
|
</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 && (
|
|
<p className="text-xs text-muted-foreground mt-0.5">
|
|
{selectedType.startDateHint}
|
|
</p>
|
|
)}
|
|
<Input
|
|
type="date"
|
|
value={startDate}
|
|
onChange={(e) => setStartDate(e.target.value)}
|
|
className="mt-1"
|
|
/>
|
|
</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 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 lucratoare"
|
|
: "zile calendaristice"}
|
|
{selectedType.isBackwardDeadline
|
|
? " INAINTE"
|
|
: " de la data start"}
|
|
</p>
|
|
{selectedType.legalReference && (
|
|
<p className="text-xs text-muted-foreground mt-1 italic">
|
|
Ref: {selectedType.legalReference}
|
|
</p>
|
|
)}
|
|
</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}>
|
|
Inapoi
|
|
</Button>
|
|
)}
|
|
{step === "category" && (
|
|
<Button type="button" variant="outline" onClick={handleClose}>
|
|
Anuleaza
|
|
</Button>
|
|
)}
|
|
{step === "date" && (
|
|
<Button
|
|
type="button"
|
|
onClick={handleConfirm}
|
|
disabled={!startDate}
|
|
>
|
|
Adauga termen
|
|
</Button>
|
|
)}
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|