feat: milestone dots on dashboard progress bar with legend
- 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 <noreply@anthropic.com>
This commit is contained in:
@@ -291,6 +291,7 @@ function EntryDeadlineCard({
|
|||||||
deadline={item.deadline}
|
deadline={item.deadline}
|
||||||
status={item.status}
|
status={item.status}
|
||||||
entryId={entry.id}
|
entryId={entry.id}
|
||||||
|
allDeadlines={entry.trackedDeadlines ?? []}
|
||||||
chainCount={chainCount}
|
chainCount={chainCount}
|
||||||
isExpanded={isExpanded}
|
isExpanded={isExpanded}
|
||||||
onToggleChain={() => onToggleChain(item.deadline.id)}
|
onToggleChain={() => onToggleChain(item.deadline.id)}
|
||||||
@@ -319,10 +320,88 @@ function EntryDeadlineCard({
|
|||||||
|
|
||||||
// ── Single deadline row ──
|
// ── 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({
|
function DeadlineRow({
|
||||||
deadline,
|
deadline,
|
||||||
status,
|
status,
|
||||||
entryId,
|
entryId,
|
||||||
|
allDeadlines,
|
||||||
chainCount,
|
chainCount,
|
||||||
isExpanded,
|
isExpanded,
|
||||||
isChainChild,
|
isChainChild,
|
||||||
@@ -332,6 +411,7 @@ function DeadlineRow({
|
|||||||
deadline: TrackedDeadline;
|
deadline: TrackedDeadline;
|
||||||
status: DeadlineDisplayStatus;
|
status: DeadlineDisplayStatus;
|
||||||
entryId: string;
|
entryId: string;
|
||||||
|
allDeadlines?: TrackedDeadline[];
|
||||||
chainCount?: number;
|
chainCount?: number;
|
||||||
isExpanded?: boolean;
|
isExpanded?: boolean;
|
||||||
isChainChild?: boolean;
|
isChainChild?: boolean;
|
||||||
@@ -358,13 +438,14 @@ function DeadlineRow({
|
|||||||
? Math.min(100, Math.max(0, (elapsedDays / totalDays) * 100))
|
? Math.min(100, Math.max(0, (elapsedDays / totalDays) * 100))
|
||||||
: 100;
|
: 100;
|
||||||
|
|
||||||
|
// Milestones on the progress bar (auto-tracked sub-deadlines)
|
||||||
|
const milestones = isPending && allDeadlines
|
||||||
|
? computeMilestones(deadline, allDeadlines)
|
||||||
|
: [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className={cn("px-4 py-2", isChainChild && "bg-muted/30 pl-8")}>
|
||||||
className={cn(
|
<div className="flex items-center gap-3">
|
||||||
"flex items-center gap-3 px-4 py-2",
|
|
||||||
isChainChild && "bg-muted/30 pl-8",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{/* Chain expand toggle */}
|
{/* Chain expand toggle */}
|
||||||
{!isChainChild && (chainCount ?? 0) > 0 ? (
|
{!isChainChild && (chainCount ?? 0) > 0 ? (
|
||||||
<button
|
<button
|
||||||
@@ -378,7 +459,7 @@ function DeadlineRow({
|
|||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
) : isChainChild ? (
|
) : isChainChild ? (
|
||||||
<span className="text-muted-foreground/40 text-xs">└</span>
|
<span className="text-muted-foreground/40 text-xs">{"\u2514"}</span>
|
||||||
) : (
|
) : (
|
||||||
<span className="w-3.5" />
|
<span className="w-3.5" />
|
||||||
)}
|
)}
|
||||||
@@ -395,10 +476,12 @@ function DeadlineRow({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Progress bar (only for pending) */}
|
{/* Progress bar with milestones (only for pending) */}
|
||||||
{isPending && (
|
{isPending && (
|
||||||
<div className="hidden w-24 sm:block">
|
<div className="hidden w-32 sm:block">
|
||||||
<div className="h-1.5 rounded-full bg-muted">
|
<div className="relative h-4">
|
||||||
|
{/* Track */}
|
||||||
|
<div className="absolute top-[7px] left-0 right-0 h-1.5 rounded-full bg-muted">
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"h-1.5 rounded-full transition-all",
|
"h-1.5 rounded-full transition-all",
|
||||||
@@ -411,6 +494,25 @@ function DeadlineRow({
|
|||||||
style={{ width: `${progressPct}%` }}
|
style={{ width: `${progressPct}%` }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
{/* Milestone dots */}
|
||||||
|
{milestones.map((ms) => (
|
||||||
|
<div
|
||||||
|
key={ms.typeId}
|
||||||
|
className="absolute top-[4px] -ml-[4px]"
|
||||||
|
style={{ left: `${ms.position * 100}%` }}
|
||||||
|
title={ms.label}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"h-2 w-2 rounded-full border",
|
||||||
|
ms.isPassed
|
||||||
|
? "border-muted-foreground/50 bg-muted-foreground/30"
|
||||||
|
: "border-amber-500 bg-amber-400",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -451,5 +553,35 @@ function DeadlineRow({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Milestone legend (below progress bar, only when milestones exist) */}
|
||||||
|
{milestones.length > 0 && isPending && (
|
||||||
|
<div className="ml-7 mt-1 hidden sm:flex flex-wrap gap-x-3 gap-y-0.5">
|
||||||
|
{milestones.map((ms) => (
|
||||||
|
<div key={ms.typeId} className="flex items-center gap-1 text-[9px]">
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"h-1.5 w-1.5 rounded-full",
|
||||||
|
ms.isPassed
|
||||||
|
? "bg-muted-foreground/40"
|
||||||
|
: "bg-amber-400",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
ms.isPassed
|
||||||
|
? "text-muted-foreground line-through"
|
||||||
|
: "text-muted-foreground",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{ms.isPassed
|
||||||
|
? getMilestoneLabel(ms.typeId, true)
|
||||||
|
: getMilestoneLabel(ms.typeId, false)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user