diff --git a/frontend/src/views/ProjectListView.vue b/frontend/src/views/ProjectListView.vue index f4573aa..dd6c1c3 100644 --- a/frontend/src/views/ProjectListView.vue +++ b/frontend/src/views/ProjectListView.vue @@ -113,6 +113,45 @@ function truncate(text: string | null, max = 120): string { return text.length > max ? text.slice(0, max) + "..." : text; } +// A card is a glance, not a report. Roundtable had ~35 milestones and its tile +// ran several viewport-heights tall, which made the grid unreadable (#2391). +const MAX_MILESTONE_BARS = 10; + +interface MilestoneBar extends MilestoneSummary { + /** Position in the FULL list, so a bar keeps its colour when another + * milestone is added or finishes. Tying the palette to the visible index + * would recolour the card every time work closed. */ + paletteIndex: number; +} + +/** Bars to draw per project, plus how many were withheld. + * + * Ordered OPEN WORK FIRST, newest first. Recency alone would be wrong here: a + * long-running project's oldest milestones are usually its finished ones, so + * showing 10 completed bars while hiding the 3 in flight is worse than showing + * nothing. What the card is for is "what is happening", not "what happened". + * + * Computed once per load rather than called from the template — a helper in a + * v-for is re-run on every render, and this one sorts. + */ +const milestoneBars = computed(() => { + const byProject = new Map(); + for (const project of projects.value) { + const all = project.summary?.milestone_summary ?? []; + const indexed: MilestoneBar[] = all.map((ms, i) => ({ ...ms, paletteIndex: i })); + const newestFirst = (a: MilestoneBar, b: MilestoneBar) => b.id - a.id; + const ordered = [ + ...indexed.filter((m) => m.pct < 100).sort(newestFirst), + ...indexed.filter((m) => m.pct >= 100).sort(newestFirst), + ]; + byProject.set(project.id, { + bars: ordered.slice(0, MAX_MILESTONE_BARS), + hidden: Math.max(0, ordered.length - MAX_MILESTONE_BARS), + }); + } + return byProject; +}); + function overallPct(project: Project): { total: number; pct: number } { const counts = project.summary?.task_counts; if (!counts) return { total: 0, pct: 0 }; @@ -185,11 +224,11 @@ function overallPct(project: Project): { total: number; pct: number } {
{{ ms.pct }}%
+ + + +{{ milestoneBars.get(project.id)!.hidden }} + {{ milestoneBars.get(project.id)!.hidden === 1 ? 'more milestone' : 'more milestones' }} +