From c892e8d820f843ae95b55e0d6f3d18595bfca3bf Mon Sep 17 00:00:00 2001 From: AI Assistant Date: Thu, 12 Mar 2026 17:54:19 +0200 Subject: [PATCH] feat: deadline pause/resume on clarifications + enhanced timeline UX MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - TrackedDeadline gains pausedAt + totalPausedDays fields - pauseDeadline() / resumeDeadline() in deadline-service with audit log - Auto-pause: when incoming conex linked to parent with active deadlines - Auto-resume: when outgoing conex linked to parent with paused deadlines (shifts dueDate forward by paused days) - Timeline shows "Suspendat" state with blue pulsing progress bar - Milestone tooltips now show exact dates (hover: "Data maximă: ...") - ISC warning text on expired emission deadlines - Verification expired text matches PDF spec - DeadlineAuditEntry gains "paused" | "resumed" action types - getDeadlineDisplayStatus returns "Suspendat" (blue) for paused deadlines Co-Authored-By: Claude Opus 4.6 --- .../components/deadline-timeline.tsx | 68 ++++++++++++++---- .../components/registratura-module.tsx | 47 +++++++++++++ .../registratura/services/deadline-service.ts | 69 +++++++++++++++++++ src/modules/registratura/types.ts | 8 ++- 4 files changed, 177 insertions(+), 15 deletions(-) diff --git a/src/modules/registratura/components/deadline-timeline.tsx b/src/modules/registratura/components/deadline-timeline.tsx index 6617c07..020127f 100644 --- a/src/modules/registratura/components/deadline-timeline.tsx +++ b/src/modules/registratura/components/deadline-timeline.tsx @@ -17,7 +17,7 @@ interface TimelineMilestone { /** Position on the timeline (0..1) */ position: number; /** Status of this milestone */ - status: "completed" | "active" | "expired" | "upcoming" | "background"; + status: "completed" | "active" | "expired" | "upcoming" | "background" | "paused"; /** Human-readable status text */ statusText: string; /** Whether this is the main (user-created) deadline or an auto-tracked milestone */ @@ -189,6 +189,7 @@ function getMainStatus( ): TimelineMilestone["status"] { if (dl.resolution === "completed") return "completed"; if (dl.resolution !== "pending") return "completed"; + if (dl.pausedAt) return "paused"; if (daysRemaining < 0) return "expired"; if (daysRemaining <= 5) return "active"; return "upcoming"; @@ -216,6 +217,8 @@ function getStatusText( switch (status) { case "completed": return "Finalizat"; + case "paused": + return "Suspendat — așteptare completări"; case "expired": return daysRemaining < 0 ? `${Math.abs(daysRemaining)}z depasit` @@ -252,7 +255,10 @@ function getExpiredDescription( status: TimelineMilestone["status"], ): string { if (status === "expired" && typeId.includes("verificare")) { - return "Posibilitatea de a solicita clarificari sau de a returna documentatia a expirat."; + return "Termenul de verificare a expirat — nu se mai pot solicita clarificari si nu se mai poate restitui dosarul."; + } + if (status === "expired" && (typeId.includes("emitere") || typeId.includes("ac-emitere"))) { + return "Termenul legal de emitere a fost depasit — poate fi sesizat ISC (sanctiuni)."; } return ""; } @@ -266,6 +272,18 @@ function formatShortDate(iso: string): string { } } +function formatFullDate(iso: string): string { + try { + return new Date(iso).toLocaleDateString("ro-RO", { + day: "2-digit", + month: "long", + year: "numeric", + }); + } catch { + return iso; + } +} + // ── Component ── export function DeadlineTimeline({ deadlines }: DeadlineTimelineProps) { @@ -290,11 +308,13 @@ export function DeadlineTimeline({ deadlines }: DeadlineTimelineProps) { "rounded-lg border px-3 py-2", main.status === "expired" ? "border-destructive/40 bg-destructive/5" - : main.status === "active" - ? "border-amber-300 bg-amber-50/30 dark:border-amber-800 dark:bg-amber-950/20" - : main.status === "completed" - ? "border-muted bg-muted/20" - : "border-border", + : main.status === "paused" + ? "border-blue-400/40 bg-blue-50/30 dark:border-blue-800 dark:bg-blue-950/20" + : main.status === "active" + ? "border-amber-300 bg-amber-50/30 dark:border-amber-800 dark:bg-amber-950/20" + : main.status === "completed" + ? "border-muted bg-muted/20" + : "border-border", )} >
@@ -319,10 +339,22 @@ export function DeadlineTimeline({ deadlines }: DeadlineTimelineProps) {
-
+
Start: {formatShortDate(main.deadline.startDate)} - Scadent: {formatShortDate(main.dueDate)} - {main.daysRemaining > 0 && main.status !== "completed" && ( + + Scadent: {formatShortDate(main.dueDate)} + + {main.status === "paused" && ( + + ⏸ Suspendat — așteptare completări + + )} + {(main.deadline.totalPausedDays ?? 0) > 0 && ( + + ({main.deadline.totalPausedDays}z suspendate) + + )} + {main.daysRemaining > 0 && main.status !== "completed" && main.status !== "paused" && ( + {main.status === "expired" && main.deadline.typeId.includes("emitere") && ( +

+ Termenul legal de emitere a fost depasit — poate fi sesizat ISC (sanctiuni) +

+ )} + {main.deadline.resolutionNote && (

{main.deadline.resolutionNote} @@ -394,9 +432,11 @@ function ProgressBar({ "h-1.5 rounded-full transition-all", status === "expired" ? "bg-red-500" - : status === "active" - ? "bg-amber-500" - : "bg-green-500", + : status === "paused" + ? "bg-blue-400 animate-pulse" + : status === "active" + ? "bg-amber-500" + : "bg-green-500", )} style={{ width: `${pct}%` }} /> @@ -432,7 +472,7 @@ function MilestoneTimeline({ key={ms.deadline.id} className="absolute top-0" style={{ left: `${ms.position * 100}%` }} - title={`${ms.label}: ${ms.statusText}`} + title={`${ms.label}\nData maximă: ${formatFullDate(ms.dueDate)}\n${ms.statusText}${ms.description ? `\n${ms.description}` : ""}`} >

dl.resolution === "pending", + ); + + if (hasPendingDeadlines) { + // Incoming conex (clarification request received) → pause deadlines + if (data.direction === "intrat") { + const hasPausable = parentDeadlines.some( + (dl) => dl.resolution === "pending" && !dl.pausedAt, + ); + if (hasPausable) { + const pausedDeadlines = parentDeadlines.map((dl) => + dl.resolution === "pending" && !dl.pausedAt + ? pauseDeadline(dl) + : dl, + ); + await updateEntry(conexToEntry.id, { + trackedDeadlines: pausedDeadlines, + }); + } + } + + // Outgoing conex (completions submitted) → resume paused deadlines + if (data.direction === "iesit") { + const hasPaused = parentDeadlines.some( + (dl) => dl.resolution === "pending" && dl.pausedAt, + ); + if (hasPaused) { + const resumedDeadlines = parentDeadlines.map((dl) => + dl.resolution === "pending" && dl.pausedAt + ? resumeDeadline(dl, data.date) + : dl, + ); + await updateEntry(conexToEntry.id, { + trackedDeadlines: resumedDeadlines, + }); + } + } + } + } + setReplyToEntry(null); setConexToEntry(null); setViewMode("list"); diff --git a/src/modules/registratura/services/deadline-service.ts b/src/modules/registratura/services/deadline-service.ts index 89b0d44..627dbe6 100644 --- a/src/modules/registratura/services/deadline-service.ts +++ b/src/modules/registratura/services/deadline-service.ts @@ -75,6 +75,70 @@ export function resolveDeadline( }; } +/** + * Pause a deadline (e.g., when a clarification request is linked). + * Records the pause date and adds audit entry. + */ +export function pauseDeadline(deadline: TrackedDeadline): TrackedDeadline { + if (deadline.pausedAt) return deadline; // already paused + const now = new Date().toISOString(); + return { + ...deadline, + pausedAt: now, + auditLog: [ + ...(deadline.auditLog ?? []), + { + action: "paused" as DeadlineAuditEntry["action"], + timestamp: now, + detail: "Termen suspendat — solicitare clarificări primită", + }, + ], + }; +} + +/** + * Resume a paused deadline (e.g., when completions are submitted). + * Adds the paused days to totalPausedDays and shifts dueDate forward. + */ +export function resumeDeadline( + deadline: TrackedDeadline, + resumeDate?: string, +): TrackedDeadline { + if (!deadline.pausedAt) return deadline; // not paused + const now = resumeDate ? new Date(resumeDate) : new Date(); + now.setHours(0, 0, 0, 0); + const pausedStart = new Date(deadline.pausedAt); + pausedStart.setHours(0, 0, 0, 0); + const pausedDays = Math.max( + 0, + Math.round((now.getTime() - pausedStart.getTime()) / (1000 * 60 * 60 * 24)), + ); + + // Shift due date forward by the number of paused days + const oldDue = new Date(deadline.dueDate); + oldDue.setHours(0, 0, 0, 0); + oldDue.setDate(oldDue.getDate() + pausedDays); + const newDueDate = formatDate(oldDue); + + const totalPaused = (deadline.totalPausedDays ?? 0) + pausedDays; + const ts = new Date().toISOString(); + + return { + ...deadline, + pausedAt: undefined, + totalPausedDays: totalPaused, + dueDate: newDueDate, + auditLog: [ + ...(deadline.auditLog ?? []), + { + action: "resumed" as DeadlineAuditEntry["action"], + timestamp: ts, + detail: `Termen reluat — ${pausedDays} zile suspendate, nou termen: ${newDueDate}`, + }, + ], + }; +} + /** * Get the display status for a tracked deadline — color coding + label. */ @@ -83,6 +147,11 @@ export function getDeadlineDisplayStatus( ): DeadlineDisplayStatus { const def = getDeadlineType(deadline.typeId); + // Paused — waiting for completions + if (deadline.pausedAt && deadline.resolution === "pending") { + return { label: "Suspendat", variant: "blue", daysRemaining: null }; + } + // Already resolved if (deadline.resolution !== "pending") { if (deadline.resolution === "aprobat-tacit") { diff --git a/src/modules/registratura/types.ts b/src/modules/registratura/types.ts index b12f684..2d4f54e 100644 --- a/src/modules/registratura/types.ts +++ b/src/modules/registratura/types.ts @@ -163,7 +163,7 @@ export interface DeadlineTypeDef { /** Audit log entry for deadline changes */ export interface DeadlineAuditEntry { - action: "created" | "resolved" | "modified" | "recipient-registered"; + action: "created" | "resolved" | "modified" | "recipient-registered" | "paused" | "resumed"; timestamp: string; /** User who performed the action (SSO name when available) */ actor?: string; @@ -182,6 +182,12 @@ export interface TrackedDeadline { /** Mini audit log — tracks who created/modified/resolved this deadline */ auditLog?: DeadlineAuditEntry[]; createdAt: string; + + // ── Pause/resume for clarification requests ── + /** ISO date when deadline was paused (solicitare clarificari linked) */ + pausedAt?: string; + /** Total calendar days the deadline has been paused across all pause periods */ + totalPausedDays?: number; } // ── AC (Autorizație de Construire) validity tracking ──