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:
@@ -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;
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user