diff --git a/src/modules/registratura/components/deadline-config-overview.tsx b/src/modules/registratura/components/deadline-config-overview.tsx new file mode 100644 index 0000000..6c3c762 --- /dev/null +++ b/src/modules/registratura/components/deadline-config-overview.tsx @@ -0,0 +1,565 @@ +"use client"; + +import { useState, useMemo } from "react"; +import { + ChevronDown, + ChevronRight, + Info, + Clock, + ShieldCheck, + Bell, + ArrowRight, + Scale, + FileWarning, + Timer, + Link2, + Zap, +} from "lucide-react"; +import { Badge } from "@/shared/components/ui/badge"; +import { Separator } from "@/shared/components/ui/separator"; +import { + DEADLINE_CATALOG, + CATEGORY_LABELS, + getDeadlineType, + getDeadlinesByCategory, +} from "../services/deadline-catalog"; +import { NOTIFICATION_TYPES } from "@/core/notifications/types"; +import type { DeadlineCategory, DeadlineTypeDef } from "../types"; +import { cn } from "@/shared/lib/utils"; + +// ── Constants ── + +const ALL_CATEGORIES: DeadlineCategory[] = [ + "certificat", + "avize", + "completari", + "urbanism", + "autorizare", + "litigii", +]; + +const CATEGORY_ICONS: Record = { + certificat: "CU", + avize: "AV", + completari: "CO", + urbanism: "UR", + autorizare: "AC", + litigii: "LI", +}; + +// ── Chain building ── + +interface ChainGroup { + sources: DeadlineTypeDef[]; + target: DeadlineTypeDef; +} + +function buildChainGroups(category: DeadlineCategory): ChainGroup[] { + const types = getDeadlinesByCategory(category); + const targetMap = new Map(); + + for (const t of types) { + if (t.chainNextTypeId) { + const sources = targetMap.get(t.chainNextTypeId) ?? []; + sources.push(t); + targetMap.set(t.chainNextTypeId, sources); + } + } + + const groups: ChainGroup[] = []; + for (const [targetId, sources] of targetMap) { + const target = getDeadlineType(targetId); + if (target) { + groups.push({ sources, target }); + } + } + return groups; +} + +// ── Legal reference index ── + +function buildLegalIndex(): Map { + const index = new Map(); + for (const t of DEADLINE_CATALOG) { + if (t.legalReference) { + const existing = index.get(t.legalReference) ?? []; + existing.push(t); + index.set(t.legalReference, existing); + } + } + return index; +} + +// ── Component ── + +export function DeadlineConfigOverview() { + const [expandedCategories, setExpandedCategories] = useState>( + () => new Set(ALL_CATEGORIES), + ); + const [expandedDescs, setExpandedDescs] = useState>(new Set()); + const [showLegalIndex, setShowLegalIndex] = useState(false); + + const stats = useMemo(() => { + const total = DEADLINE_CATALOG.length; + const autoTracked = DEADLINE_CATALOG.filter((d) => d.autoTrack).length; + const tacit = DEADLINE_CATALOG.filter((d) => d.tacitApprovalApplicable).length; + const chained = DEADLINE_CATALOG.filter((d) => d.chainNextTypeId).length; + return { total, autoTracked, tacit, chained }; + }, []); + + const legalIndex = useMemo(() => buildLegalIndex(), []); + + const toggleCategory = (cat: DeadlineCategory) => { + setExpandedCategories((prev) => { + const next = new Set(prev); + if (next.has(cat)) next.delete(cat); + else next.add(cat); + return next; + }); + }; + + const toggleDesc = (id: string) => { + setExpandedDescs((prev) => { + const next = new Set(prev); + if (next.has(id)) next.delete(id); + else next.add(id); + return next; + }); + }; + + return ( +
+ {/* ── Summary stats ── */} +
+ } value={stats.total} label="Tipuri termene" /> + } value={stats.autoTracked} label="Auto-tracked" /> + } value={stats.tacit} label="Aprobare tacita" /> + } value={stats.chained} label="Termene in lant" /> +
+ + {/* ── Legend ── */} +
+ Legenda: + + Auto + + + Manual + + + Tacit + + + Lucratoare + + + Calendaristice + + + Fundal + +
+ + {/* ── Category sections ── */} + {ALL_CATEGORIES.map((cat) => { + const types = getDeadlinesByCategory(cat); + if (types.length === 0) return null; + const isExpanded = expandedCategories.has(cat); + const chainGroups = buildChainGroups(cat); + + return ( +
+ {/* Category header */} + + + {/* Category body */} + {isExpanded && ( +
+ +
+ {types.map((t) => ( + toggleDesc(t.id)} + /> + ))} +
+ + {/* Chain flow diagrams */} + {chainGroups.length > 0 && ( +
+

+ Fluxuri de termene in lant +

+
+ {chainGroups.map((group) => ( + + ))} +
+
+ )} +
+ )} +
+ ); + })} + + {/* ── Notification overview ── */} +
+
+ + Notificari email +
+ +
+

+ Digest zilnic trimis luni-vineri la 8:00 prin Brevo SMTP, declansat automat de N8N cron. +

+
+ {NOTIFICATION_TYPES.map((nt) => ( +
+ +
+ {nt.label} + + {nt.description} + +
+
+ ))} +
+

+ Preferintele per utilizator se configureaza din iconita clopotel din bara de sus. +

+
+
+ + {/* ── Document expiry overview ── */} +
+
+ + Expirare documente +
+ +
+
+
+ +
+ Certificat de Urbanism (CU) + + — data expirare + fereastra de alerta (implicit 30 zile). Se configureaza per inregistrare. + +
+
+
+ +
+ Autorizatie de Construire (AC) + + — validitate 12 luni de la emitere, executie 6/12/24/36 luni, prelungire +24 luni. + Reminder lunar automat (snooze/dismiss disponibil). + +
+
+
+ +
+ Monitorizare externa + + — verificare automata status la autoritate (Primaria Cluj etc.). Notificare la schimbare status. + +
+
+
+

+ Transmiterea in termen (1 zi) se verifica automat in fundal cand un entry e inchis prin act administrativ. +

+
+
+ + {/* ── Legal reference index ── */} +
+ + {showLegalIndex && ( + <> + +
+ {Array.from(legalIndex.entries()) + .sort(([a], [b]) => a.localeCompare(b)) + .map(([ref, types]) => ( +
+ {ref} + + {types.map((t) => t.label).join(", ")} + +
+ ))} +
+ + )} +
+
+ ); +} + +// ── Stat card ── + +function StatCard({ + icon, + value, + label, +}: { + icon: React.ReactNode; + value: number; + label: string; +}) { + return ( +
+ {icon} +
+

{value}

+

{label}

+
+
+ ); +} + +// ── Deadline type row ── + +function DeadlineTypeRow({ + type, + descExpanded, + onToggleDesc, +}: { + type: DeadlineTypeDef; + descExpanded: boolean; + onToggleDesc: () => void; +}) { + const chainTarget = type.chainNextTypeId + ? getDeadlineType(type.chainNextTypeId) + : undefined; + + return ( +
+
+ {/* Label */} + {type.label} + + {/* Duration badge */} + + + {type.days}z {type.dayType === "working" ? "lucr." : "cal."} + + + {/* Auto/Manual badge */} + {type.autoTrack ? ( + + Auto + + ) : ( + + Manual + + )} + + {/* Tacit badge */} + {type.tacitApprovalApplicable && ( + + + Tacit + + )} + + {/* Background badge */} + {type.backgroundOnly && ( + + Fundal + + )} + + {/* Direction filter */} + {type.directionFilter && type.directionFilter.length === 1 && ( + + {type.directionFilter[0] === "iesit" ? "Iesit" : "Intrat"} + + )} + + {/* Info toggle */} + +
+ + {/* Chain indicator */} + {chainTarget && ( +
+ + + La rezolvare lanseaza: {chainTarget.label} + {" "}({chainTarget.days}z {chainTarget.dayType === "working" ? "lucr." : "cal."}) + +
+ )} + + {/* Description + legal ref (expanded) */} + {descExpanded && ( +
+

+ {type.description} +

+ {type.legalReference && ( +

+ + {type.legalReference} +

+ )} + {type.startDateHint && ( +

+ + {type.startDateHint} +

+ )} +
+ )} +
+ ); +} + +// ── Chain flow diagram ── + +function ChainFlowDiagram({ group }: { group: ChainGroup }) { + const isFanIn = group.sources.length > 1; + + if (isFanIn) { + return ( +
+ {/* Multiple sources stacked */} +
+ {group.sources.map((src) => ( + + ))} +
+ {/* Converging arrows */} +
+
+ {group.sources.map((_, i) => ( +
+ ))} +
+
+
+
+ {/* Target */} + +
+ ); + } + + // Single chain: source → target + const src = group.sources[0]; + if (!src) return null; + + return ( +
+ +
+
+
+
+ +
+ ); +} + +function ChainNode({ + type, + isTarget, + small, +}: { + type: DeadlineTypeDef; + isTarget?: boolean; + small?: boolean; +}) { + return ( +
+

+ {type.label} +

+

+ {type.days}z {type.dayType === "working" ? "lucr." : "cal."} + {type.tacitApprovalApplicable && ( + tacit + )} +

+
+ ); +} diff --git a/src/modules/registratura/components/registratura-module.tsx b/src/modules/registratura/components/registratura-module.tsx index 51d722e..95bfa1d 100644 --- a/src/modules/registratura/components/registratura-module.tsx +++ b/src/modules/registratura/components/registratura-module.tsx @@ -38,6 +38,7 @@ import { RegistryTable } from "./registry-table"; import { RegistryEntryForm } from "./registry-entry-form"; import { RegistryEntryDetail } from "./registry-entry-detail"; import { DeadlineDashboard } from "./deadline-dashboard"; +import { DeadlineConfigOverview } from "./deadline-config-overview"; import { ThreadExplorer } from "./thread-explorer"; import { CloseGuardDialog } from "./close-guard-dialog"; import { getOverdueDays } from "../services/registry-service"; @@ -81,6 +82,7 @@ export function RegistraturaModule() { const [conexToEntry, setConexToEntry] = useState(null); /** If set, the parent entry will be closed after saving the new reply entry */ const [closesEntryId, setClosesEntryId] = useState(null); + const [termeneView, setTermeneView] = useState<"active" | "guide">("active"); const [closingId, setClosingId] = useState(null); const [linkCheckId, setLinkCheckId] = useState(null); @@ -630,11 +632,34 @@ export function RegistraturaModule() { - + {/* Sub-navigation: active deadlines vs. system guide */} +
+ + +
+ + {termeneView === "active" ? ( + + ) : ( + + )}
);