diff --git a/src/modules/registratura/components/deadline-table.tsx b/src/modules/registratura/components/deadline-table.tsx deleted file mode 100644 index f9b5ea0..0000000 --- a/src/modules/registratura/components/deadline-table.tsx +++ /dev/null @@ -1,127 +0,0 @@ -'use client'; - -import { CheckCircle2 } from 'lucide-react'; -import { Badge } from '@/shared/components/ui/badge'; -import { Button } from '@/shared/components/ui/button'; -import type { TrackedDeadline, RegistryEntry } from '../types'; -import type { DeadlineDisplayStatus } from '../services/deadline-service'; -import { getDeadlineType, CATEGORY_LABELS } from '../services/deadline-catalog'; -import { cn } from '@/shared/lib/utils'; - -interface DeadlineRow { - deadline: TrackedDeadline; - entry: RegistryEntry; - status: DeadlineDisplayStatus; -} - -interface DeadlineTableProps { - rows: DeadlineRow[]; - onResolve: (entryId: string, deadline: TrackedDeadline) => void; -} - -const BADGE_CLASSES: Record = { - green: 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200 border-0', - yellow: 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200 border-0', - red: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200 border-0', - blue: 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200 border-0', - gray: 'bg-muted text-muted-foreground border-0', -}; - -const ROW_CLASSES: Record = { - red: 'bg-destructive/5', - yellow: 'bg-yellow-50/50 dark:bg-yellow-950/10', - blue: 'bg-blue-50/50 dark:bg-blue-950/10', -}; - -export function DeadlineTable({ rows, onResolve }: DeadlineTableProps) { - if (rows.length === 0) { - return ( -

- Niciun termen legal urmărit. -

- ); - } - - return ( -
- - - - - - - - - - - - - - - {rows.map((row) => { - const def = getDeadlineType(row.deadline.typeId); - return ( - - - - - - - - - - - ); - })} - -
Nr. înreg.CategorieTip termenData startTermen limităZileStatusAcțiuni
{row.entry.number} - {def ? CATEGORY_LABELS[def.category] : '—'} - - {def?.label ?? row.deadline.typeId} - {def?.dayType === 'working' && ( - (lucr.) - )} - {formatDate(row.deadline.startDate)}{formatDate(row.deadline.dueDate)} - {row.status.daysRemaining !== null ? ( - - {row.status.daysRemaining < 0 - ? `${Math.abs(row.status.daysRemaining)}z depășit` - : `${row.status.daysRemaining}z`} - - ) : ( - '—' - )} - - - {row.status.label} - - - {row.deadline.resolution === 'pending' && ( - - )} -
-
- ); -} - -function formatDate(iso: string): string { - try { - return new Date(iso).toLocaleDateString('ro-RO', { day: '2-digit', month: '2-digit', year: 'numeric' }); - } catch { - return iso; - } -} diff --git a/src/modules/registratura/components/deadline-timeline.tsx b/src/modules/registratura/components/deadline-timeline.tsx new file mode 100644 index 0000000..6617c07 --- /dev/null +++ b/src/modules/registratura/components/deadline-timeline.tsx @@ -0,0 +1,515 @@ +"use client"; + +import { useMemo } from "react"; +import { Badge } from "@/shared/components/ui/badge"; +import type { TrackedDeadline } from "../types"; +import { getDeadlineType } from "../services/deadline-catalog"; +import { getDeadlineDisplayStatus } from "../services/deadline-service"; +import { cn } from "@/shared/lib/utils"; + +// ── Types ── + +interface TimelineMilestone { + deadline: TrackedDeadline; + label: string; + description: string; + dueDate: string; + /** Position on the timeline (0..1) */ + position: number; + /** Status of this milestone */ + status: "completed" | "active" | "expired" | "upcoming" | "background"; + /** Human-readable status text */ + statusText: string; + /** Whether this is the main (user-created) deadline or an auto-tracked milestone */ + isMain: boolean; + /** Days remaining (negative = overdue) */ + daysRemaining: number; +} + +interface DeadlineTimelineProps { + /** All tracked deadlines on this entry */ + deadlines: TrackedDeadline[]; +} + +// ── Build timeline from deadlines ── + +function buildTimeline(deadlines: TrackedDeadline[]): { + mainDeadlines: TimelineMilestone[]; + groups: Map; +} { + const mainDeadlines: TimelineMilestone[] = []; + // Group auto-tracked by their chain parent (or by category proximity to a main deadline) + const groups = new Map(); + + const now = new Date(); + now.setHours(0, 0, 0, 0); + + // Separate main vs auto-tracked deadlines + const mainDls: TrackedDeadline[] = []; + const autoDls: TrackedDeadline[] = []; + + for (const dl of deadlines) { + const def = getDeadlineType(dl.typeId); + if (def?.backgroundOnly) continue; // Skip background-only entirely + if (def?.autoTrack) { + autoDls.push(dl); + } else { + mainDls.push(dl); + } + } + + // For each main deadline, find its associated milestones + for (const main of mainDls) { + const def = getDeadlineType(main.typeId); + const mainStart = new Date(main.startDate); + mainStart.setHours(0, 0, 0, 0); + const mainDue = new Date(main.dueDate); + mainDue.setHours(0, 0, 0, 0); + const totalSpan = Math.max(1, mainDue.getTime() - mainStart.getTime()); + const mainDaysRemaining = Math.ceil( + (mainDue.getTime() - now.getTime()) / (1000 * 60 * 60 * 24), + ); + + const mainStatus = getMainStatus(main, mainDaysRemaining); + + mainDeadlines.push({ + deadline: main, + label: def?.label ?? main.typeId, + description: def?.description ?? "", + dueDate: main.dueDate, + position: 1, + status: mainStatus, + statusText: getStatusText(mainStatus, mainDaysRemaining), + isMain: true, + daysRemaining: mainDaysRemaining, + }); + + // Find auto-tracked milestones that fall within this main deadline's range + const milestones: TimelineMilestone[] = []; + for (const auto of autoDls) { + const autoDef = getDeadlineType(auto.typeId); + if (!autoDef) continue; + + // Check if this auto-tracked deadline is related (same start date or chain parent in this group) + const autoStart = new Date(auto.startDate); + autoStart.setHours(0, 0, 0, 0); + const autoDue = new Date(auto.dueDate); + autoDue.setHours(0, 0, 0, 0); + + // Only include if it starts from the same point or is within the main's time range + const startsNearMain = + Math.abs(autoStart.getTime() - mainStart.getTime()) < + 2 * 24 * 60 * 60 * 1000; + const isChainRelated = auto.chainParentId + ? deadlines.some( + (d) => + d.id === auto.chainParentId && + (d.typeId === main.typeId || + d.chainParentId === main.id), + ) + : false; + + if (!startsNearMain && !isChainRelated) continue; + + const pos = Math.min( + 0.95, + Math.max( + 0.05, + (autoDue.getTime() - mainStart.getTime()) / totalSpan, + ), + ); + const autoDaysRemaining = Math.ceil( + (autoDue.getTime() - now.getTime()) / (1000 * 60 * 60 * 24), + ); + const autoStatus = getAutoStatus(auto, autoDaysRemaining, autoDef.id); + + milestones.push({ + deadline: auto, + label: autoDef.label, + description: getExpiredDescription(autoDef.id, autoStatus), + dueDate: auto.dueDate, + position: pos, + status: autoStatus, + statusText: getAutoStatusText(autoDef.id, autoStatus, autoDaysRemaining), + isMain: false, + daysRemaining: autoDaysRemaining, + }); + } + + // Sort milestones by position + milestones.sort((a, b) => a.position - b.position); + if (milestones.length > 0) { + groups.set(main.id, milestones); + } + } + + // Handle auto-tracked deadlines that aren't associated with any main deadline + const associatedAutoIds = new Set(); + for (const ms of groups.values()) { + for (const m of ms) { + associatedAutoIds.add(m.deadline.id); + } + } + const orphanedAuto = autoDls.filter( + (dl) => !associatedAutoIds.has(dl.id), + ); + + if (orphanedAuto.length > 0 && mainDls.length === 0) { + // If there are ONLY auto-tracked deadlines (no main), show them as standalone milestones + for (const auto of orphanedAuto) { + const autoDef = getDeadlineType(auto.typeId); + if (!autoDef) continue; + const autoDue = new Date(auto.dueDate); + autoDue.setHours(0, 0, 0, 0); + const daysRemaining = Math.ceil( + (autoDue.getTime() - now.getTime()) / (1000 * 60 * 60 * 24), + ); + const status = getMainStatus(auto, daysRemaining); + + mainDeadlines.push({ + deadline: auto, + label: autoDef.label, + description: autoDef.description, + dueDate: auto.dueDate, + position: 1, + status, + statusText: getStatusText(status, daysRemaining), + isMain: true, + daysRemaining, + }); + } + } + + return { mainDeadlines, groups }; +} + +function getMainStatus( + dl: TrackedDeadline, + daysRemaining: number, +): TimelineMilestone["status"] { + if (dl.resolution === "completed") return "completed"; + if (dl.resolution !== "pending") return "completed"; + if (daysRemaining < 0) return "expired"; + if (daysRemaining <= 5) return "active"; + return "upcoming"; +} + +function getAutoStatus( + dl: TrackedDeadline, + daysRemaining: number, + typeId: string, +): TimelineMilestone["status"] { + if (dl.resolution !== "pending") return "completed"; + if (daysRemaining < 0) { + // For verification-type deadlines, "expired" means the window has passed + if (typeId.includes("verificare")) return "expired"; + return "expired"; + } + if (daysRemaining <= 2) return "active"; + return "upcoming"; +} + +function getStatusText( + status: TimelineMilestone["status"], + daysRemaining: number, +): string { + switch (status) { + case "completed": + return "Finalizat"; + case "expired": + return daysRemaining < 0 + ? `${Math.abs(daysRemaining)}z depasit` + : "Depasit"; + case "active": + return daysRemaining === 0 + ? "Astazi" + : `${daysRemaining}z ramase`; + case "upcoming": + return `${daysRemaining}z ramase`; + default: + return ""; + } +} + +function getAutoStatusText( + typeId: string, + status: TimelineMilestone["status"], + daysRemaining: number, +): string { + if (status === "completed") return "Finalizat"; + if (status === "expired") { + if (typeId.includes("verificare")) { + return "Expirat — nu se mai pot solicita clarificari"; + } + return `Depasit cu ${Math.abs(daysRemaining)}z`; + } + if (daysRemaining === 0) return "Astazi"; + return `${daysRemaining}z`; +} + +function getExpiredDescription( + typeId: string, + status: TimelineMilestone["status"], +): string { + if (status === "expired" && typeId.includes("verificare")) { + return "Posibilitatea de a solicita clarificari sau de a returna documentatia a expirat."; + } + return ""; +} + +function formatShortDate(iso: string): string { + try { + const d = new Date(iso); + return `${String(d.getDate()).padStart(2, "0")}.${String(d.getMonth() + 1).padStart(2, "0")}`; + } catch { + return iso; + } +} + +// ── Component ── + +export function DeadlineTimeline({ deadlines }: DeadlineTimelineProps) { + const { mainDeadlines, groups } = useMemo( + () => buildTimeline(deadlines), + [deadlines], + ); + + if (mainDeadlines.length === 0) return null; + + return ( +
+ {mainDeadlines.map((main) => { + const milestones = groups.get(main.deadline.id) ?? []; + const displayStatus = getDeadlineDisplayStatus(main.deadline); + + return ( +
+ {/* Main deadline header */} +
+
+ {main.label} + + {displayStatus.label} + +
+ +
+ Start: {formatShortDate(main.deadline.startDate)} + Scadent: {formatShortDate(main.dueDate)} + {main.daysRemaining > 0 && main.status !== "completed" && ( + + {main.daysRemaining}z ramase + + )} + {main.daysRemaining < 0 && main.status === "expired" && ( + + {Math.abs(main.daysRemaining)}z depasit + + )} +
+ + {main.deadline.resolutionNote && ( +

+ {main.deadline.resolutionNote} +

+ )} + + {/* Progress bar */} + {main.status !== "completed" && ( + + )} + + {/* Milestones timeline */} + {milestones.length > 0 && ( + + )} +
+
+ ); + })} +
+ ); +} + +// ── Progress bar ── + +function ProgressBar({ + deadline, + status, +}: { + deadline: TrackedDeadline; + status: TimelineMilestone["status"]; +}) { + const now = new Date(); + now.setHours(0, 0, 0, 0); + const start = new Date(deadline.startDate); + start.setHours(0, 0, 0, 0); + const due = new Date(deadline.dueDate); + due.setHours(0, 0, 0, 0); + + const total = Math.max(1, due.getTime() - start.getTime()); + const elapsed = now.getTime() - start.getTime(); + const pct = Math.min(100, Math.max(0, (elapsed / total) * 100)); + + return ( +
+
+
+ ); +} + +// ── Milestone timeline (horizontal dots along the progress bar) ── + +function MilestoneTimeline({ + milestones, + mainDeadline, +}: { + milestones: TimelineMilestone[]; + mainDeadline: TrackedDeadline; +}) { + return ( +
+ {/* Timeline track */} +
+ {/* Base line */} +
+ + {/* Start marker */} +
+ + {/* End marker (main deadline) */} +
+ + {/* Milestone dots */} + {milestones.map((ms) => ( +
+
+ {ms.status === "completed" && ( + + ✓ + + )} + {ms.status === "expired" && ( + + )} +
+
+ ))} +
+ + {/* Milestone labels below */} +
+ {milestones.map((ms) => ( +
+ +
+
+ + {ms.label} + + + {formatShortDate(ms.dueDate)} + + + {ms.statusText} + +
+ {ms.description && ( +

{ms.description}

+ )} +
+
+ ))} +
+
+ ); +} diff --git a/src/modules/registratura/components/registratura-module.tsx b/src/modules/registratura/components/registratura-module.tsx index 50a2328..51d722e 100644 --- a/src/modules/registratura/components/registratura-module.tsx +++ b/src/modules/registratura/components/registratura-module.tsx @@ -168,18 +168,37 @@ export function RegistraturaModule() { ); } - const closureInfo: ClosureInfo = { + const parentClosureInfo: ClosureInfo = { resolution: "finalizat", reason: "Inchis prin inregistrare conex", closedBy: "", closedAt: new Date().toISOString(), hadActiveDeadlines: resolvable.length > 0, + linkedEntryId: newEntry?.id, + linkedEntryNumber: newEntry?.number, }; await updateEntry(closesEntryId, { - closureInfo, + closureInfo: parentClosureInfo, trackedDeadlines: updatedDeadlines, }); await closeEntry(closesEntryId, false); + + // Also close the new entry (act administrativ) — it was created to close the parent + if (newEntry) { + const replyClosureInfo: ClosureInfo = { + resolution: "finalizat", + reason: `Act administrativ emis — inchide ${parentEntry.number}`, + closedBy: "", + closedAt: new Date().toISOString(), + hadActiveDeadlines: false, + linkedEntryId: parentEntry.id, + linkedEntryNumber: parentEntry.number, + }; + await updateEntry(newEntry.id, { + status: "inchis", + closureInfo: replyClosureInfo, + }); + } } setClosesEntryId(null); } diff --git a/src/modules/registratura/components/registry-entry-detail.tsx b/src/modules/registratura/components/registry-entry-detail.tsx index 348b86c..3982328 100644 --- a/src/modules/registratura/components/registry-entry-detail.tsx +++ b/src/modules/registratura/components/registry-entry-detail.tsx @@ -52,6 +52,7 @@ import { findAuthorityForContact } from "../services/authority-catalog"; import { computeTransmissionStatus } from "../services/deadline-service"; import { StatusMonitorConfig } from "./status-monitor-config"; import { FlowDiagram } from "./flow-diagram"; +import { DeadlineTimeline } from "./deadline-timeline"; interface RegistryEntryDetailProps { entry: RegistryEntry | null; @@ -584,58 +585,10 @@ export function RegistryEntryDetail({ )} - {/* ── Legal deadlines ── */} + {/* ── Legal deadlines (timeline view) ── */} {(entry.trackedDeadlines ?? []).length > 0 && ( - -
- {entry.trackedDeadlines!.map((dl) => ( -
-
- {dl.typeId} - - {DEADLINE_RES_LABELS[dl.resolution] ?? dl.resolution} - -
-
- - - Start: {formatDate(dl.startDate)} - - - - Scadent: {formatDate(dl.dueDate)} - -
- {dl.resolutionNote && ( -

- {dl.resolutionNote} -

- )} -
- ))} -
+ + )} diff --git a/src/modules/registratura/hooks/use-deadline-filters.ts b/src/modules/registratura/hooks/use-deadline-filters.ts deleted file mode 100644 index ec48619..0000000 --- a/src/modules/registratura/hooks/use-deadline-filters.ts +++ /dev/null @@ -1,28 +0,0 @@ -'use client'; - -import { useState, useCallback } from 'react'; -import type { DeadlineCategory, DeadlineResolution } from '../types'; - -export interface DeadlineFilters { - category: DeadlineCategory | 'all'; - resolution: DeadlineResolution | 'all'; - urgentOnly: boolean; -} - -export function useDeadlineFilters() { - const [filters, setFilters] = useState({ - category: 'all', - resolution: 'all', - urgentOnly: false, - }); - - const updateFilter = useCallback((key: K, value: DeadlineFilters[K]) => { - setFilters((prev) => ({ ...prev, [key]: value })); - }, []); - - const resetFilters = useCallback(() => { - setFilters({ category: 'all', resolution: 'all', urgentOnly: false }); - }, []); - - return { filters, updateFilter, resetFilters }; -} diff --git a/src/modules/registratura/services/deadline-service.ts b/src/modules/registratura/services/deadline-service.ts index a73e699..c7f21df 100644 --- a/src/modules/registratura/services/deadline-service.ts +++ b/src/modules/registratura/services/deadline-service.ts @@ -275,14 +275,24 @@ export function groupDeadlinesByEntry( } } + // Check if there are any user-selectable (non-autoTrack) deadlines + const hasUserDeadlines = deadlines.some((dl) => { + const d = getDeadlineType(dl.typeId); + return !d?.autoTrack && !d?.backgroundOnly && !dl.chainParentId; + }); + // Second pass: build main deadline items for (const dl of deadlines) { const def = getDeadlineType(dl.typeId); if (def?.backgroundOnly) continue; if (dl.chainParentId) continue; // chain children are nested - // Auto-tracked without chain parent: treat as main if pending, skip if resolved - if (def?.autoTrack && dl.resolution !== "pending") continue; + // Auto-tracked: hide when there are user-selectable deadlines (they appear as milestones) + // Only show auto-tracked as standalone when there are NO user deadlines on the entry + if (def?.autoTrack) { + if (hasUserDeadlines) continue; + if (dl.resolution !== "pending") continue; + } mainItems.push({ deadline: dl,