fix(registratura): 5 post-3.02 fixes - QuickContact pre-fill (useEffect sync), form close on contact create (stopPropagation), Switch label 'Inchis' -> 'Status', CU moved from Avize to own category, close guard for active deadlines

This commit is contained in:
AI Assistant
2026-02-27 16:02:10 +02:00
parent 2be0462e0d
commit 80e41d4842
8 changed files with 470 additions and 131 deletions
+1
View File
@@ -264,6 +264,7 @@
- **Legături între intrări/ieșiri (Thread-uri & Branches):** Posibilitatea de a lega o ieșire de o intrare specifică (ex: "Răspuns la adresa nr. X"), creând un "fir" (thread) vizual al conversației instituționale. Trebuie să suporte și "branching" (ex: o intrare generează mai multe ieșiri conexe către instituții diferite). UI-ul trebuie să rămână extrem de simplu și intuitiv (ex: vizualizare tip arbore simplificat sau listă indentată în detaliile documentului). - **Legături între intrări/ieșiri (Thread-uri & Branches):** Posibilitatea de a lega o ieșire de o intrare specifică (ex: "Răspuns la adresa nr. X"), creând un "fir" (thread) vizual al conversației instituționale. Trebuie să suporte și "branching" (ex: o intrare generează mai multe ieșiri conexe către instituții diferite). UI-ul trebuie să rămână extrem de simplu și intuitiv (ex: vizualizare tip arbore simplificat sau listă indentată în detaliile documentului).
**Status:** ✅ Done. All 6 sub-features implemented: **Status:** ✅ Done. All 6 sub-features implemented:
1. **Dynamic doc types** — Select + inline "Tip nou" input. New types auto-created as Tag Manager tags (category: document-type). Added "Apel telefonic" and "Videoconferință" as defaults. 1. **Dynamic doc types** — Select + inline "Tip nou" input. New types auto-created as Tag Manager tags (category: document-type). Added "Apel telefonic" and "Videoconferință" as defaults.
2. **Bidirectional Address Book** — Autocomplete shows "Creează contact" button when no match. QuickContactDialog popup creates contact in Address Book with minimal data (Name required, Phone/Email optional). 2. **Bidirectional Address Book** — Autocomplete shows "Creează contact" button when no match. QuickContactDialog popup creates contact in Address Book with minimal data (Name required, Phone/Email optional).
3. **Simplified status** — Replaced Status dropdown with Switch toggle "Închis/Deschis". Default is open. 3. **Simplified status** — Replaced Status dropdown with Switch toggle "Închis/Deschis". Default is open.
+4
View File
@@ -46,6 +46,7 @@ Continued Phase 3 refinements. Picked task 3.02 (HEAVY) — Registratura bidirec
## Session — 2026-02-27 evening (GitHub Copilot - Claude Opus 4.6) ## Session — 2026-02-27 evening (GitHub Copilot - Claude Opus 4.6)
### Context ### Context
Continued from earlier session (Gemini 3.1 Pro got stuck on Authentik testing). This session focused on fixing deployment pipeline, then implementing Phase 3 visual/UX tasks + infrastructure documentation. Continued from earlier session (Gemini 3.1 Pro got stuck on Authentik testing). This session focused on fixing deployment pipeline, then implementing Phase 3 visual/UX tasks + infrastructure documentation.
### Completed ### Completed
@@ -85,9 +86,11 @@ Continued from earlier session (Gemini 3.1 Pro got stuck on Authentik testing).
- SESSION-LOG.md: this entry - SESSION-LOG.md: this entry
### Files Created ### Files Created
- `src/shared/components/common/theme-toggle.tsx` — Animated sun/moon theme toggle - `src/shared/components/common/theme-toggle.tsx` — Animated sun/moon theme toggle
### Files Modified ### Files Modified
- `Dockerfile` — Added `npx prisma generate` - `Dockerfile` — Added `npx prisma generate`
- `docker-compose.yml` — All env vars hardcoded - `docker-compose.yml` — All env vars hardcoded
- `package.json`@prisma/client moved to dependencies - `package.json`@prisma/client moved to dependencies
@@ -101,6 +104,7 @@ Continued from earlier session (Gemini 3.1 Pro got stuck on Authentik testing).
- `src/modules/hot-desk/components/desk-room-layout.tsx` — Window + door landmarks - `src/modules/hot-desk/components/desk-room-layout.tsx` — Window + door landmarks
### Notes ### Notes
- Portainer CE requires manual "Pull and redeploy" — no auto-rebuild on webhook - Portainer CE requires manual "Pull and redeploy" — no auto-rebuild on webhook
- "Re-pull image" checkbox only needed for base image updates (node:20-alpine), not for code changes - "Re-pull image" checkbox only needed for base image updates (node:20-alpine), not for code changes
- Logo SVGs have very different aspect ratios (BTG ~7:1, US ~6:1, SDT ~3:1) — using flex-1 min-w-0 to handle this - Logo SVGs have very different aspect ratios (BTG ~7:1, US ~6:1, SDT ~3:1) — using flex-1 min-w-0 to handle this
@@ -0,0 +1,252 @@
"use client";
import { useState, useMemo } from "react";
import { AlertTriangle, Search, Link2 } from "lucide-react";
import { Button } from "@/shared/components/ui/button";
import { Input } from "@/shared/components/ui/input";
import { Label } from "@/shared/components/ui/label";
import { Textarea } from "@/shared/components/ui/textarea";
import { Badge } from "@/shared/components/ui/badge";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter,
} from "@/shared/components/ui/dialog";
import type { RegistryEntry, TrackedDeadline } from "../types";
import { getDeadlineType } from "../services/deadline-catalog";
interface CloseGuardDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
/** The entry being closed */
entry: RegistryEntry;
/** All entries for search/linking */
allEntries: RegistryEntry[];
/** Active deadlines on this entry */
activeDeadlines: TrackedDeadline[];
/** Called when user provides justification and confirms close */
onConfirmClose: (justification: {
linkedEntryId?: string;
reason: string;
}) => void;
}
/**
* Guard dialog shown when a user tries to close an entry that has
* unresolved (active) legal deadlines. Requires justification:
* either link to a continuation entry or provide manual reason text.
*/
export function CloseGuardDialog({
open,
onOpenChange,
entry,
allEntries,
activeDeadlines,
onConfirmClose,
}: CloseGuardDialogProps) {
const [mode, setMode] = useState<"link" | "manual">("link");
const [search, setSearch] = useState("");
const [selectedEntryId, setSelectedEntryId] = useState("");
const [reason, setReason] = useState("");
// Reset on open
const handleOpenChange = (o: boolean) => {
if (o) {
setMode("link");
setSearch("");
setSelectedEntryId("");
setReason("");
}
onOpenChange(o);
};
// Searchable entries (exclude self and closed entries)
const searchResults = useMemo(() => {
if (!search || search.length < 2) return [];
const q = search.toLowerCase();
return allEntries
.filter(
(e) =>
e.id !== entry.id &&
e.status !== "inchis" &&
(e.subject.toLowerCase().includes(q) ||
e.number.toLowerCase().includes(q) ||
e.sender.toLowerCase().includes(q) ||
e.recipient.toLowerCase().includes(q)),
)
.slice(0, 8);
}, [search, allEntries, entry.id]);
const selectedEntry = allEntries.find((e) => e.id === selectedEntryId);
const canSubmit =
mode === "link"
? selectedEntryId !== ""
: reason.trim().length >= 5;
const handleSubmit = () => {
if (!canSubmit) return;
onConfirmClose({
linkedEntryId: mode === "link" ? selectedEntryId : undefined,
reason:
mode === "link"
? `Continuat în #${selectedEntry?.number ?? selectedEntryId}`
: reason.trim(),
});
};
return (
<Dialog open={open} onOpenChange={handleOpenChange}>
<DialogContent className="sm:max-w-lg">
<DialogHeader>
<DialogTitle className="flex items-center gap-2 text-amber-600 dark:text-amber-400">
<AlertTriangle className="h-5 w-5" />
Termene legale active
</DialogTitle>
</DialogHeader>
<div className="space-y-4">
{/* Warning message */}
<p className="text-sm text-muted-foreground">
Înregistrarea <strong>{entry.number}</strong> are{" "}
<strong>{activeDeadlines.length}</strong> termen
{activeDeadlines.length > 1 ? "e" : ""} legal
{activeDeadlines.length > 1 ? "e" : ""} nerezolvat
{activeDeadlines.length > 1 ? "e" : ""}. Pentru a o închide,
justifică decizia.
</p>
{/* Active deadlines list */}
<div className="rounded border p-2 space-y-1.5">
{activeDeadlines.map((dl) => {
const def = getDeadlineType(dl.typeId);
const isOverdue = new Date(dl.dueDate) < new Date();
return (
<div
key={dl.id}
className="flex items-center gap-2 text-sm"
>
<Badge
variant={isOverdue ? "destructive" : "outline"}
className="text-[10px] shrink-0"
>
{isOverdue ? "Depășit" : "Activ"}
</Badge>
<span className="font-medium">
{def?.label ?? dl.typeId}
</span>
<span className="text-muted-foreground text-xs ml-auto">
scadent {dl.dueDate}
</span>
</div>
);
})}
</div>
{/* Mode toggle */}
<div className="flex gap-2">
<Button
type="button"
variant={mode === "link" ? "default" : "outline"}
size="sm"
onClick={() => setMode("link")}
className="flex-1"
>
<Link2 className="mr-1.5 h-3.5 w-3.5" />
Leagă de altă înregistrare
</Button>
<Button
type="button"
variant={mode === "manual" ? "default" : "outline"}
size="sm"
onClick={() => setMode("manual")}
className="flex-1"
>
Justificare manuală
</Button>
</div>
{/* Link mode */}
{mode === "link" && (
<div className="space-y-2">
<Label>Caută înregistrarea-continuare</Label>
<div className="relative">
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
<Input
value={search}
onChange={(e) => {
setSearch(e.target.value);
setSelectedEntryId("");
}}
placeholder="Caută după nr., subiect, expeditor..."
className="pl-9"
/>
</div>
{searchResults.length > 0 && (
<div className="max-h-48 overflow-y-auto rounded border divide-y">
{searchResults.map((e) => (
<button
key={e.id}
type="button"
className={`w-full px-3 py-2 text-left text-sm hover:bg-accent transition-colors ${
selectedEntryId === e.id ? "bg-accent" : ""
}`}
onClick={() => setSelectedEntryId(e.id)}
>
<span className="font-mono font-medium text-xs">
{e.number}
</span>
<span className="ml-2">{e.subject}</span>
<span className="ml-2 text-muted-foreground text-xs">
{e.sender}
</span>
</button>
))}
</div>
)}
{selectedEntry && (
<p className="text-xs text-muted-foreground">
Selectat: <strong>{selectedEntry.number}</strong> {" "}
{selectedEntry.subject}
</p>
)}
</div>
)}
{/* Manual mode */}
{mode === "manual" && (
<div className="space-y-2">
<Label>Motiv închidere (min. 5 caractere)</Label>
<Textarea
value={reason}
onChange={(e) => setReason(e.target.value)}
placeholder="Ex: Dosarul a fost retras de beneficiar..."
rows={3}
/>
</div>
)}
</div>
<DialogFooter>
<Button
type="button"
variant="outline"
onClick={() => onOpenChange(false)}
>
Anulează
</Button>
<Button
type="button"
onClick={handleSubmit}
disabled={!canSubmit}
variant="destructive"
>
Închide înregistrarea
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
@@ -1,6 +1,6 @@
"use client"; "use client";
import { useState } from "react"; import { useState, useEffect } from "react";
import { UserPlus } from "lucide-react"; import { UserPlus } from "lucide-react";
import { Input } from "@/shared/components/ui/input"; import { Input } from "@/shared/components/ui/input";
import { Label } from "@/shared/components/ui/label"; import { Label } from "@/shared/components/ui/label";
@@ -35,24 +35,28 @@ export function QuickContactDialog({
const [phone, setPhone] = useState(""); const [phone, setPhone] = useState("");
const [email, setEmail] = useState(""); const [email, setEmail] = useState("");
// Reset when dialog opens with new name // Sync name with initialName whenever the dialog opens
const handleOpenChange = (o: boolean) => { // (useState(initialName) only works on first mount; controlled open
if (o) { // bypasses onOpenChange so we need useEffect)
useEffect(() => {
if (open) {
setName(initialName); setName(initialName);
setPhone(""); setPhone("");
setEmail(""); setEmail("");
} }
onOpenChange(o); }, [open, initialName]);
};
const handleSubmit = (e: React.FormEvent) => { const handleSubmit = (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
// Stop propagation so the submit doesn't bubble through the React portal
// to the outer registry-entry-form, which would close the whole form.
e.stopPropagation();
if (!name.trim()) return; if (!name.trim()) return;
onConfirm({ name: name.trim(), phone: phone.trim(), email: email.trim() }); onConfirm({ name: name.trim(), phone: phone.trim(), email: email.trim() });
}; };
return ( return (
<Dialog open={open} onOpenChange={handleOpenChange}> <Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-md"> <DialogContent className="sm:max-w-md">
<DialogHeader> <DialogHeader>
<DialogTitle className="flex items-center gap-2"> <DialogTitle className="flex items-center gap-2">
@@ -30,6 +30,7 @@ import { RegistryFilters } from "./registry-filters";
import { RegistryTable } from "./registry-table"; import { RegistryTable } from "./registry-table";
import { RegistryEntryForm } from "./registry-entry-form"; import { RegistryEntryForm } from "./registry-entry-form";
import { DeadlineDashboard } from "./deadline-dashboard"; import { DeadlineDashboard } from "./deadline-dashboard";
import { CloseGuardDialog } from "./close-guard-dialog";
import { getOverdueDays } from "../services/registry-service"; import { getOverdueDays } from "../services/registry-service";
import { aggregateDeadlines } from "../services/deadline-service"; import { aggregateDeadlines } from "../services/deadline-service";
import type { RegistryEntry, DeadlineResolution } from "../types"; import type { RegistryEntry, DeadlineResolution } from "../types";
@@ -59,6 +60,7 @@ export function RegistraturaModule() {
const [viewMode, setViewMode] = useState<ViewMode>("list"); const [viewMode, setViewMode] = useState<ViewMode>("list");
const [editingEntry, setEditingEntry] = useState<RegistryEntry | null>(null); const [editingEntry, setEditingEntry] = useState<RegistryEntry | null>(null);
const [closingId, setClosingId] = useState<string | null>(null); const [closingId, setClosingId] = useState<string | null>(null);
const [guardingId, setGuardingId] = useState<string | null>(null);
// ── Bidirectional Address Book integration ── // ── Bidirectional Address Book integration ──
const handleCreateContact = useCallback( const handleCreateContact = useCallback(
@@ -143,13 +145,51 @@ export function RegistraturaModule() {
const handleCloseRequest = (id: string) => { const handleCloseRequest = (id: string) => {
const entry = allEntries.find((e) => e.id === id); const entry = allEntries.find((e) => e.id === id);
if (entry && (entry.linkedEntryIds ?? []).length > 0) { if (!entry) return;
// Check for active (unresolved) legal deadlines
const activeDeadlines = (entry.trackedDeadlines ?? []).filter(
(d) => d.resolution === "pending",
);
if (activeDeadlines.length > 0) {
// Show guard dialog first
setGuardingId(id);
return;
}
// No active deadlines — proceed to linked-entries check
if ((entry.linkedEntryIds ?? []).length > 0) {
setClosingId(id); setClosingId(id);
} else { } else {
closeEntry(id, false); closeEntry(id, false);
} }
}; };
/** Called after the close-guard dialog is confirmed */
const handleGuardConfirm = (justification: {
linkedEntryId?: string;
reason: string;
}) => {
if (!guardingId) return;
const entry = allEntries.find((e) => e.id === guardingId);
// Store the justification in notes
if (entry) {
const justNote = `\n[Închis cu termene active] ${justification.reason}`;
updateEntry(guardingId, {
notes: (entry.notes ?? "") + justNote,
});
}
// Now check for linked entries
if (entry && (entry.linkedEntryIds ?? []).length > 0) {
setClosingId(guardingId);
} else {
closeEntry(guardingId, false);
}
setGuardingId(null);
};
const handleCloseConfirm = (closeLinked: boolean) => { const handleCloseConfirm = (closeLinked: boolean) => {
if (closingId) { if (closingId) {
closeEntry(closingId, closeLinked); closeEntry(closingId, closeLinked);
@@ -202,6 +242,15 @@ export function RegistraturaModule() {
? allEntries.find((e) => e.id === closingId) ? allEntries.find((e) => e.id === closingId)
: null; : null;
const guardingEntry = guardingId
? allEntries.find((e) => e.id === guardingId)
: null;
const guardingActiveDeadlines = guardingEntry
? (guardingEntry.trackedDeadlines ?? []).filter(
(d) => d.resolution === "pending",
)
: [];
return ( return (
<Tabs defaultValue="registru"> <Tabs defaultValue="registru">
<TabsList> <TabsList>
@@ -333,6 +382,20 @@ export function RegistraturaModule() {
</DialogFooter> </DialogFooter>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
{/* Close guard for entries with active legal deadlines */}
{guardingEntry && (
<CloseGuardDialog
open={guardingId !== null}
onOpenChange={(open) => {
if (!open) setGuardingId(null);
}}
entry={guardingEntry}
allEntries={allEntries}
activeDeadlines={guardingActiveDeadlines}
onConfirmClose={handleGuardConfirm}
/>
)}
</div> </div>
</TabsContent> </TabsContent>
@@ -609,7 +609,7 @@ export function RegistryEntryForm({
</div> </div>
<div> <div>
<Label className="flex items-center gap-1.5"> <Label className="flex items-center gap-1.5">
Închis Status
<TooltipProvider> <TooltipProvider>
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
@@ -1,220 +1,234 @@
import type { DeadlineTypeDef, DeadlineCategory } from '../types'; import type { DeadlineTypeDef, DeadlineCategory } from "../types";
export const DEADLINE_CATALOG: DeadlineTypeDef[] = [ export const DEADLINE_CATALOG: DeadlineTypeDef[] = [
// ── Certificat de Urbanism ──
{
id: "cerere-cu",
label: "Cerere CU",
description:
"Termen de emitere a Certificatului de Urbanism de la data depunerii cererii.",
days: 15,
dayType: "calendar",
startDateLabel: "Data depunerii",
requiresCustomStartDate: false,
tacitApprovalApplicable: true,
category: "certificat",
legalReference: "Legea 50/1991, art. 6¹",
},
// ── Avize ── // ── Avize ──
{ {
id: 'cerere-cu', id: "avize-normale",
label: 'Cerere CU', label: "Cerere Avize normale",
description: 'Termen de emitere a Certificatului de Urbanism de la data depunerii cererii.', description: "Termen de emitere a avizelor de la data depunerii cererii.",
days: 15, days: 15,
dayType: 'calendar', dayType: "calendar",
startDateLabel: 'Data depunerii', startDateLabel: "Data depunerii",
requiresCustomStartDate: false, requiresCustomStartDate: false,
tacitApprovalApplicable: true, tacitApprovalApplicable: true,
category: 'avize', category: "avize",
legalReference: 'Legea 50/1991, art. 6¹',
}, },
{ {
id: 'avize-normale', id: "aviz-cultura",
label: 'Cerere Avize normale', label: "Aviz Cultură",
description: 'Termen de emitere a avizelor de la data depunerii cererii.', description:
days: 15, "Termen de emitere a avizului Ministerului Culturii de la data comisiei.",
dayType: 'calendar',
startDateLabel: 'Data depunerii',
requiresCustomStartDate: false,
tacitApprovalApplicable: true,
category: 'avize',
},
{
id: 'aviz-cultura',
label: 'Aviz Cultură',
description: 'Termen de emitere a avizului Ministerului Culturii de la data comisiei.',
days: 30, days: 30,
dayType: 'calendar', dayType: "calendar",
startDateLabel: 'Data comisie', startDateLabel: "Data comisie",
requiresCustomStartDate: true, requiresCustomStartDate: true,
startDateHint: 'Data ședinței comisiei de specialitate', startDateHint: "Data ședinței comisiei de specialitate",
tacitApprovalApplicable: true, tacitApprovalApplicable: true,
category: 'avize', category: "avize",
}, },
{ {
id: 'aviz-mediu', id: "aviz-mediu",
label: 'Aviz Mediu', label: "Aviz Mediu",
description: 'Termen de emitere a avizului de mediu de la finalizarea procedurilor.', description:
"Termen de emitere a avizului de mediu de la finalizarea procedurilor.",
days: 15, days: 15,
dayType: 'calendar', dayType: "calendar",
startDateLabel: 'Data finalizare proceduri', startDateLabel: "Data finalizare proceduri",
requiresCustomStartDate: true, requiresCustomStartDate: true,
startDateHint: 'Data finalizării procedurii de evaluare de mediu', startDateHint: "Data finalizării procedurii de evaluare de mediu",
tacitApprovalApplicable: true, tacitApprovalApplicable: true,
category: 'avize', category: "avize",
}, },
{ {
id: 'aviz-aeronautica', id: "aviz-aeronautica",
label: 'Aviz Aeronautică', label: "Aviz Aeronautică",
description: 'Termen de emitere a avizului de la Autoritatea Aeronautică.', description: "Termen de emitere a avizului de la Autoritatea Aeronautică.",
days: 30, days: 30,
dayType: 'calendar', dayType: "calendar",
startDateLabel: 'Data depunerii', startDateLabel: "Data depunerii",
requiresCustomStartDate: false, requiresCustomStartDate: false,
tacitApprovalApplicable: true, tacitApprovalApplicable: true,
category: 'avize', category: "avize",
}, },
// ── Completări ── // ── Completări ──
{ {
id: 'completare-beneficiar', id: "completare-beneficiar",
label: 'Completare — termen beneficiar', label: "Completare — termen beneficiar",
description: 'Termen acordat beneficiarului pentru completarea documentației.', description:
"Termen acordat beneficiarului pentru completarea documentației.",
days: 60, days: 60,
dayType: 'calendar', dayType: "calendar",
startDateLabel: 'Data notificării', startDateLabel: "Data notificării",
requiresCustomStartDate: false, requiresCustomStartDate: false,
tacitApprovalApplicable: false, tacitApprovalApplicable: false,
chainNextTypeId: 'completare-emitere', chainNextTypeId: "completare-emitere",
chainNextActionLabel: 'Adaugă termen emitere (15 zile)', chainNextActionLabel: "Adaugă termen emitere (15 zile)",
category: 'completari', category: "completari",
}, },
{ {
id: 'completare-emitere', id: "completare-emitere",
label: 'Completare — termen emitere', label: "Completare — termen emitere",
description: 'Termen de emitere după depunerea completărilor.', description: "Termen de emitere după depunerea completărilor.",
days: 15, days: 15,
dayType: 'calendar', dayType: "calendar",
startDateLabel: 'Data depunere completări', startDateLabel: "Data depunere completări",
requiresCustomStartDate: true, requiresCustomStartDate: true,
startDateHint: 'Data la care beneficiarul a depus completările', startDateHint: "Data la care beneficiarul a depus completările",
tacitApprovalApplicable: true, tacitApprovalApplicable: true,
category: 'completari', category: "completari",
}, },
// ── Analiză ── // ── Analiză ──
{ {
id: 'ctatu-analiza', id: "ctatu-analiza",
label: 'Analiză CTATU', label: "Analiză CTATU",
description: 'Termen de analiză în Comisia Tehnică de Amenajare a Teritoriului și Urbanism.', description:
"Termen de analiză în Comisia Tehnică de Amenajare a Teritoriului și Urbanism.",
days: 30, days: 30,
dayType: 'calendar', dayType: "calendar",
startDateLabel: 'Data depunerii', startDateLabel: "Data depunerii",
requiresCustomStartDate: false, requiresCustomStartDate: false,
tacitApprovalApplicable: false, tacitApprovalApplicable: false,
category: 'analiza', category: "analiza",
}, },
{ {
id: 'consiliu-promovare', id: "consiliu-promovare",
label: 'Promovare Consiliu Local', label: "Promovare Consiliu Local",
description: 'Termen de promovare în ședința Consiliului Local.', description: "Termen de promovare în ședința Consiliului Local.",
days: 30, days: 30,
dayType: 'calendar', dayType: "calendar",
startDateLabel: 'Data depunerii', startDateLabel: "Data depunerii",
requiresCustomStartDate: false, requiresCustomStartDate: false,
tacitApprovalApplicable: false, tacitApprovalApplicable: false,
category: 'analiza', category: "analiza",
}, },
{ {
id: 'consiliu-vot', id: "consiliu-vot",
label: 'Vot Consiliu Local', label: "Vot Consiliu Local",
description: 'Termen de vot în Consiliu Local de la finalizarea dezbaterii publice.', description:
"Termen de vot în Consiliu Local de la finalizarea dezbaterii publice.",
days: 45, days: 45,
dayType: 'calendar', dayType: "calendar",
startDateLabel: 'Data finalizare dezbatere', startDateLabel: "Data finalizare dezbatere",
requiresCustomStartDate: true, requiresCustomStartDate: true,
startDateHint: 'Data finalizării dezbaterii publice', startDateHint: "Data finalizării dezbaterii publice",
tacitApprovalApplicable: false, tacitApprovalApplicable: false,
category: 'analiza', category: "analiza",
}, },
// ── Autorizare ── // ── Autorizare ──
{ {
id: 'verificare-ac', id: "verificare-ac",
label: 'Verificare AC', label: "Verificare AC",
description: 'Termen de verificare a documentației pentru Autorizația de Construire.', description:
"Termen de verificare a documentației pentru Autorizația de Construire.",
days: 5, days: 5,
dayType: 'working', dayType: "working",
startDateLabel: 'Data depunerii', startDateLabel: "Data depunerii",
requiresCustomStartDate: false, requiresCustomStartDate: false,
tacitApprovalApplicable: false, tacitApprovalApplicable: false,
category: 'autorizare', category: "autorizare",
}, },
{ {
id: 'prelungire-ac', id: "prelungire-ac",
label: 'Cerere prelungire AC', label: "Cerere prelungire AC",
description: 'Cererea de prelungire trebuie depusă cu minim 45 zile lucrătoare ÎNAINTE de expirarea AC.', description:
"Cererea de prelungire trebuie depusă cu minim 45 zile lucrătoare ÎNAINTE de expirarea AC.",
days: 45, days: 45,
dayType: 'working', dayType: "working",
startDateLabel: 'Data expirare AC', startDateLabel: "Data expirare AC",
requiresCustomStartDate: true, requiresCustomStartDate: true,
startDateHint: 'Data de expirare a Autorizației de Construire', startDateHint: "Data de expirare a Autorizației de Construire",
tacitApprovalApplicable: false, tacitApprovalApplicable: false,
category: 'autorizare', category: "autorizare",
isBackwardDeadline: true, isBackwardDeadline: true,
}, },
{ {
id: 'prelungire-ac-comunicare', id: "prelungire-ac-comunicare",
label: 'Comunicare decizie prelungire', label: "Comunicare decizie prelungire",
description: 'Termen de comunicare a deciziei privind prelungirea AC.', description: "Termen de comunicare a deciziei privind prelungirea AC.",
days: 15, days: 15,
dayType: 'working', dayType: "working",
startDateLabel: 'Data depunere cerere', startDateLabel: "Data depunere cerere",
requiresCustomStartDate: true, requiresCustomStartDate: true,
startDateHint: 'Data depunerii cererii de prelungire', startDateHint: "Data depunerii cererii de prelungire",
tacitApprovalApplicable: false, tacitApprovalApplicable: false,
category: 'autorizare', category: "autorizare",
}, },
// ── Publicitate ── // ── Publicitate ──
{ {
id: 'publicitate-ac', id: "publicitate-ac",
label: 'Publicitate AC', label: "Publicitate AC",
description: 'Termen de publicitate a Autorizației de Construire.', description: "Termen de publicitate a Autorizației de Construire.",
days: 30, days: 30,
dayType: 'calendar', dayType: "calendar",
startDateLabel: 'Data emitere AC', startDateLabel: "Data emitere AC",
requiresCustomStartDate: true, requiresCustomStartDate: true,
startDateHint: 'Data emiterii Autorizației de Construire', startDateHint: "Data emiterii Autorizației de Construire",
tacitApprovalApplicable: false, tacitApprovalApplicable: false,
category: 'publicitate', category: "publicitate",
}, },
{ {
id: 'plangere-prealabila', id: "plangere-prealabila",
label: 'Plângere prealabilă', label: "Plângere prealabilă",
description: 'Termen de depunere a plângerii prealabile.', description: "Termen de depunere a plângerii prealabile.",
days: 30, days: 30,
dayType: 'calendar', dayType: "calendar",
startDateLabel: 'Data ultimă publicitate', startDateLabel: "Data ultimă publicitate",
requiresCustomStartDate: true, requiresCustomStartDate: true,
startDateHint: 'Data ultimei publicități / aduceri la cunoștință', startDateHint: "Data ultimei publicități / aduceri la cunoștință",
tacitApprovalApplicable: false, tacitApprovalApplicable: false,
chainNextTypeId: 'contestare-instanta', chainNextTypeId: "contestare-instanta",
chainNextActionLabel: 'Adaugă termen contestare instanță (60 zile)', chainNextActionLabel: "Adaugă termen contestare instanță (60 zile)",
category: 'publicitate', category: "publicitate",
}, },
{ {
id: 'contestare-instanta', id: "contestare-instanta",
label: 'Contestare în instanță', label: "Contestare în instanță",
description: 'Termen de contestare în instanța de contencios administrativ.', description:
"Termen de contestare în instanța de contencios administrativ.",
days: 60, days: 60,
dayType: 'calendar', dayType: "calendar",
startDateLabel: 'Data răspuns plângere', startDateLabel: "Data răspuns plângere",
requiresCustomStartDate: true, requiresCustomStartDate: true,
startDateHint: 'Data primirii răspunsului la plângerea prealabilă', startDateHint: "Data primirii răspunsului la plângerea prealabilă",
tacitApprovalApplicable: false, tacitApprovalApplicable: false,
category: 'publicitate', category: "publicitate",
}, },
]; ];
export const CATEGORY_LABELS: Record<DeadlineCategory, string> = { export const CATEGORY_LABELS: Record<DeadlineCategory, string> = {
avize: 'Avize', certificat: "Certificat de Urbanism",
completari: 'Completări', avize: "Avize",
analiza: 'Analiză', completari: "Completări",
autorizare: 'Autorizare', analiza: "Analiză",
publicitate: 'Publicitate', autorizare: "Autorizare",
publicitate: "Publicitate",
}; };
export function getDeadlineType(typeId: string): DeadlineTypeDef | undefined { export function getDeadlineType(typeId: string): DeadlineTypeDef | undefined {
return DEADLINE_CATALOG.find((d) => d.id === typeId); return DEADLINE_CATALOG.find((d) => d.id === typeId);
} }
export function getDeadlinesByCategory(category: DeadlineCategory): DeadlineTypeDef[] { export function getDeadlinesByCategory(
category: DeadlineCategory,
): DeadlineTypeDef[] {
return DEADLINE_CATALOG.filter((d) => d.category === category); return DEADLINE_CATALOG.filter((d) => d.category === category);
} }
+1
View File
@@ -63,6 +63,7 @@ export type DeadlineResolution =
| "anulat"; | "anulat";
export type DeadlineCategory = export type DeadlineCategory =
| "certificat"
| "avize" | "avize"
| "completari" | "completari"
| "analiza" | "analiza"