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 "";
|
if (!text) return "";
|
||||||
return text.length > max ? text.slice(0, max) + "..." : text;
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -164,6 +172,18 @@ function truncate(text: string | null, max = 120): string {
|
|||||||
</p>
|
</p>
|
||||||
<p v-if="project.description" class="project-desc">{{ truncate(project.description) }}</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 -->
|
<!-- Milestone progress bars -->
|
||||||
<div
|
<div
|
||||||
v-if="project.summary?.milestone_summary?.length"
|
v-if="project.summary?.milestone_summary?.length"
|
||||||
@@ -417,6 +437,34 @@ function truncate(text: string | null, max = 120): string {
|
|||||||
line-height: 1.45;
|
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 {
|
.milestone-bars {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
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() {
|
async function loadProject() {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
error.value = null;
|
error.value = null;
|
||||||
@@ -130,6 +138,7 @@ async function loadProject() {
|
|||||||
editStatus.value = data.status;
|
editStatus.value = data.status;
|
||||||
editDirty.value = false;
|
editDirty.value = false;
|
||||||
milestones.value = data.summary?.milestone_summary ?? [];
|
milestones.value = data.summary?.milestone_summary ?? [];
|
||||||
|
autoCollapseCompleted(milestones.value);
|
||||||
} catch {
|
} catch {
|
||||||
error.value = "Failed to load project.";
|
error.value = "Failed to load project.";
|
||||||
} finally {
|
} finally {
|
||||||
@@ -143,6 +152,7 @@ async function loadMilestones() {
|
|||||||
`/api/projects/${projectId.value}/milestones`
|
`/api/projects/${projectId.value}/milestones`
|
||||||
);
|
);
|
||||||
milestones.value = data.milestones;
|
milestones.value = data.milestones;
|
||||||
|
autoCollapseCompleted(milestones.value);
|
||||||
} catch {
|
} catch {
|
||||||
// silent
|
// silent
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user