feat: overall completion bar on project cards; auto-collapse done milestones

ProjectListView: add an overall task completion bar above the per-milestone
bars showing the percentage of all project tasks that are done.

ProjectView: milestones where every task is complete now start collapsed
by default, keeping the view clean for projects with many finished stages.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-10 08:38:02 -04:00
parent 2f5abc034e
commit 3b8d40fea3
2 changed files with 58 additions and 0 deletions
+48
View File
@@ -113,6 +113,14 @@ function truncate(text: string | null, max = 120): string {
if (!text) return "";
return text.length > max ? text.slice(0, max) + "..." : text;
}
function overallPct(project: Project): { total: number; pct: number } {
const counts = project.summary?.task_counts;
if (!counts) return { total: 0, pct: 0 };
const total = (counts.todo ?? 0) + (counts.in_progress ?? 0) + (counts.done ?? 0);
const pct = total > 0 ? Math.round((counts.done ?? 0) / total * 100) : 0;
return { total, pct };
}
</script>
<template>
@@ -164,6 +172,18 @@ function truncate(text: string | null, max = 120): string {
</p>
<p v-if="project.description" class="project-desc">{{ truncate(project.description) }}</p>
<!-- Overall completion bar -->
<div
v-if="overallPct(project).total > 0"
class="overall-bar-row"
:title="`${project.summary?.task_counts.done ?? 0} of ${overallPct(project).total} tasks complete`"
>
<div class="overall-bar-track">
<div class="overall-bar-fill" :style="{ width: overallPct(project).pct + '%' }"></div>
</div>
<span class="overall-bar-pct">{{ overallPct(project).pct }}%</span>
</div>
<!-- Milestone progress bars -->
<div
v-if="project.summary?.milestone_summary?.length"
@@ -417,6 +437,34 @@ function truncate(text: string | null, max = 120): string {
line-height: 1.45;
}
.overall-bar-row {
display: flex;
align-items: center;
gap: 0.4rem;
font-size: 0.72rem;
margin-top: 0.1rem;
}
.overall-bar-track {
flex: 1;
height: 6px;
background: var(--color-border);
border-radius: 999px;
overflow: hidden;
}
.overall-bar-fill {
height: 100%;
border-radius: 999px;
background: var(--color-primary);
transition: width 0.3s ease;
}
.overall-bar-pct {
color: var(--color-text-secondary);
flex-shrink: 0;
min-width: 2.5rem;
text-align: right;
font-weight: 600;
}
.milestone-bars {
display: flex;
flex-direction: column;
+10
View File
@@ -118,6 +118,14 @@ function toggleMilestoneCollapse(id: number) {
}
}
function autoCollapseCompleted(msList: Milestone[]) {
for (const ms of msList) {
if (ms.total > 0 && ms.completed === ms.total) {
collapsedMilestones.value.add(ms.id);
}
}
}
async function loadProject() {
loading.value = true;
error.value = null;
@@ -130,6 +138,7 @@ async function loadProject() {
editStatus.value = data.status;
editDirty.value = false;
milestones.value = data.summary?.milestone_summary ?? [];
autoCollapseCompleted(milestones.value);
} catch {
error.value = "Failed to load project.";
} finally {
@@ -143,6 +152,7 @@ async function loadMilestones() {
`/api/projects/${projectId.value}/milestones`
);
milestones.value = data.milestones;
autoCollapseCompleted(milestones.value);
} catch {
// silent
}