From 0cc14a96e969a43c81e197075ae5336d4b9191f7 Mon Sep 17 00:00:00 2001 From: AI Assistant Date: Thu, 12 Mar 2026 00:12:26 +0200 Subject: [PATCH] feat: milestone dots on dashboard progress bar with legend MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Progress bar now shows auto-tracked sub-deadline milestones as dots - Passed milestones: grayed out dot + strikethrough label - Active milestones: amber dot with descriptive label - Each milestone has a tooltip with context-aware text (e.g., "Verificare expirata — nu se mai pot solicita clarificari") - Legend below progress bar shows all milestone labels - Wider progress bar (w-32) to accommodate milestone dots Co-Authored-By: Claude Opus 4.6 --- .../components/deadline-dashboard.tsx | 304 +++++++++++++----- 1 file changed, 218 insertions(+), 86 deletions(-) diff --git a/src/modules/registratura/components/deadline-dashboard.tsx b/src/modules/registratura/components/deadline-dashboard.tsx index bf4ac82..c0f386c 100644 --- a/src/modules/registratura/components/deadline-dashboard.tsx +++ b/src/modules/registratura/components/deadline-dashboard.tsx @@ -291,6 +291,7 @@ function EntryDeadlineCard({ deadline={item.deadline} status={item.status} entryId={entry.id} + allDeadlines={entry.trackedDeadlines ?? []} chainCount={chainCount} isExpanded={isExpanded} onToggleChain={() => onToggleChain(item.deadline.id)} @@ -319,10 +320,88 @@ function EntryDeadlineCard({ // ── Single deadline row ── +// ── Milestone data for progress bar ── + +interface ProgressMilestone { + position: number; // 0..1 on the progress bar + label: string; + isPassed: boolean; + typeId: string; +} + +function computeMilestones( + mainDeadline: TrackedDeadline, + allDeadlines: TrackedDeadline[], +): ProgressMilestone[] { + const mainStart = new Date(mainDeadline.startDate).getTime(); + const mainDue = new Date(mainDeadline.dueDate).getTime(); + const totalSpan = mainDue - mainStart; + if (totalSpan <= 0) return []; + + const now = Date.now(); + const milestones: ProgressMilestone[] = []; + + for (const dl of allDeadlines) { + if (dl.id === mainDeadline.id) continue; + const def = getDeadlineType(dl.typeId); + if (!def?.autoTrack) continue; + if (def.backgroundOnly) continue; + + const dlDue = new Date(dl.dueDate).getTime(); + // Only show milestones that fall within the main deadline's time range + if (dlDue <= mainStart || dlDue >= mainDue) continue; + + const pos = (dlDue - mainStart) / totalSpan; + if (pos < 0.03 || pos > 0.97) continue; // too close to edges + + milestones.push({ + position: pos, + label: getMilestoneLabel(def.id, dlDue < now), + isPassed: dlDue < now, + typeId: def.id, + }); + } + + milestones.sort((a, b) => a.position - b.position); + return milestones; +} + +function getMilestoneLabel(typeId: string, isPassed: boolean): string { + if (typeId.includes("verificare")) { + return isPassed + ? "Verificare expirata — nu se mai pot solicita clarificari" + : "Termen verificare cerere"; + } + if (typeId.includes("completari-limit")) { + return isPassed + ? "Termen solicitare completari expirat" + : "Limita solicitare completari"; + } + if (typeId.includes("comunicare")) { + return isPassed ? "Termen comunicare expirat" : "Termen comunicare"; + } + if (typeId.includes("solicitare-aviz")) { + return isPassed + ? "Termen solicitare aviz expirat" + : "Solicitare aviz primar"; + } + if (typeId.includes("aviz-primar")) { + return isPassed ? "Aviz primar expirat" : "Aviz primar"; + } + if (typeId.includes("depunere-comisie")) { + return isPassed ? "Depunere comisie expirata" : "Depunere la comisie"; + } + const def = getDeadlineType(typeId); + return def?.label ?? typeId; +} + +// ── Single deadline row ── + function DeadlineRow({ deadline, status, entryId, + allDeadlines, chainCount, isExpanded, isChainChild, @@ -332,6 +411,7 @@ function DeadlineRow({ deadline: TrackedDeadline; status: DeadlineDisplayStatus; entryId: string; + allDeadlines?: TrackedDeadline[]; chainCount?: number; isExpanded?: boolean; isChainChild?: boolean; @@ -358,98 +438,150 @@ function DeadlineRow({ ? Math.min(100, Math.max(0, (elapsedDays / totalDays) * 100)) : 100; - return ( -
- {/* Chain expand toggle */} - {!isChainChild && (chainCount ?? 0) > 0 ? ( - - ) : isChainChild ? ( - - ) : ( - - )} + // Milestones on the progress bar (auto-tracked sub-deadlines) + const milestones = isPending && allDeadlines + ? computeMilestones(deadline, allDeadlines) + : []; - {/* Type label */} -
- - {def?.label ?? deadline.typeId} - - {!isChainChild && (chainCount ?? 0) > 0 && ( - - (+{chainCount} etap{chainCount === 1 ? "a" : "e"}) - + return ( +
+
+ {/* Chain expand toggle */} + {!isChainChild && (chainCount ?? 0) > 0 ? ( + + ) : isChainChild ? ( + {"\u2514"} + ) : ( + )} + + {/* Type label */} +
+ + {def?.label ?? deadline.typeId} + + {!isChainChild && (chainCount ?? 0) > 0 && ( + + (+{chainCount} etap{chainCount === 1 ? "a" : "e"}) + + )} +
+ + {/* Progress bar with milestones (only for pending) */} + {isPending && ( +
+
+ {/* Track */} +
+
+
+ {/* Milestone dots */} + {milestones.map((ms) => ( +
+
+
+ ))} +
+
+ )} + + {/* Countdown / status */} +
+ {isPending && status.daysRemaining !== null ? ( + + {status.daysRemaining < 0 + ? `${Math.abs(status.daysRemaining)}z depasit` + : status.daysRemaining === 0 + ? "azi" + : `${status.daysRemaining}z ramase`} + + ) : ( + + {status.label} + + )} +
+ + {/* Resolve button */} +
+ {isPending && ( + + )} +
- {/* Progress bar (only for pending) */} - {isPending && ( -
-
-
-
+ {/* Milestone legend (below progress bar, only when milestones exist) */} + {milestones.length > 0 && isPending && ( +
+ {milestones.map((ms) => ( +
+ + + {ms.isPassed + ? getMilestoneLabel(ms.typeId, true) + : getMilestoneLabel(ms.typeId, false)} + +
+ ))}
)} - - {/* Countdown / status */} -
- {isPending && status.daysRemaining !== null ? ( - - {status.daysRemaining < 0 - ? `${Math.abs(status.daysRemaining)}z depasit` - : status.daysRemaining === 0 - ? "azi" - : `${status.daysRemaining}z ramase`} - - ) : ( - - {status.label} - - )} -
- - {/* Resolve button */} -
- {isPending && ( - - )} -
); }