Fix inconsistent and confusing project statistics display

Backend:
- Count print job records instead of summing quantities for stats
- Include "archived" status as successful (completed + archived)
- Include all failure states (failed, aborted, cancelled, stopped)
- Add completed_count and failed_count to ProjectListResponse

Frontend ProjectsPage:
- Show completed_count instead of total_items in progress displays
- Progress bar now shows "X / Y completed" to match percentage
- Add failed count indicator (red) when failures exist
- Footer stats show completed (green checkmark) and failed (red) separately

Frontend ProjectDetailPage:
- Replace confusing "Total Items" card with "Print Jobs" card
- Show "X successful" as main value, "Y failed" below in red
- Show "Z in progress" in yellow for unfinished prints
- Progress bar text changed from "items" to "completed"
- Add hint prop to StatCard component for tooltips

This ensures stats are consistent between:
- Project list page and detail page
- Stats cards and archive list counts
- Progress percentages and displayed fractions

Fixes #85
This commit is contained in:
maziggy
2026-01-14 16:26:58 +01:00
parent 3dbbd7ea19
commit c7377bfb0c
3 changed files with 59 additions and 34 deletions
+3 -1
View File
@@ -412,7 +412,9 @@ export interface ProjectListItem {
target_count: number | null;
created_at: string;
archive_count: number; // Number of print jobs
total_items: number; // Sum of quantities (total items printed)
total_items: number; // Sum of quantities (total items printed, including failed)
completed_count: number; // Sum of quantities for completed prints only
failed_count: number; // Sum of quantities for failed prints
queue_count: number;
progress_percent: number | null;
archives: ArchivePreview[];
+25 -19
View File
@@ -79,18 +79,20 @@ function StatCard({
label,
value,
subValue,
hint,
color = 'text-bambu-gray',
}: {
icon: React.ElementType;
label: string;
value: string | number;
subValue?: string;
hint?: string;
color?: string;
}) {
return (
<Card>
<CardContent className="p-4">
<div className="flex items-center gap-3">
<div className="flex items-center gap-3" title={hint}>
<div className={`p-2 rounded-lg bg-bambu-dark ${color}`}>
<Icon className="w-5 h-5" />
</div>
@@ -435,9 +437,6 @@ export function ProjectDetailPage() {
const stats = project.stats;
const progressPercent = stats?.progress_percent ?? 0;
const successRate = stats && stats.total_items > 0
? ((stats.completed_prints / stats.total_items) * 100).toFixed(0)
: null;
return (
<div className="p-4 md:p-8 space-y-8">
@@ -486,7 +485,7 @@ export function ProjectDetailPage() {
<div className="flex items-center justify-between mb-2">
<span className="text-sm text-bambu-gray">Progress</span>
<span className="text-sm font-medium text-white">
{stats?.completed_prints || 0} / {project.target_count} items
{stats?.completed_prints || 0} / {project.target_count} completed
</span>
</div>
<div className="h-3 bg-bambu-dark rounded-full overflow-hidden">
@@ -515,20 +514,25 @@ export function ProjectDetailPage() {
{/* Stats grid */}
{stats && (
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<StatCard
icon={Package}
label="Total Items"
value={stats.total_items}
subValue={`${stats.total_archives} print job${stats.total_archives !== 1 ? 's' : ''}`}
color="text-bambu-green"
/>
<StatCard
icon={CheckCircle}
label="Completed"
value={stats.completed_prints}
subValue={stats.failed_prints > 0 ? `${stats.failed_prints} failed` : (successRate ? `${successRate}% success` : undefined)}
color="text-blue-400"
/>
<Card>
<CardContent className="p-4">
<div className="flex items-center gap-3">
<div className="p-2 rounded-lg bg-bambu-dark text-bambu-green">
<Package className="w-5 h-5" />
</div>
<div>
<p className="text-sm text-bambu-gray">Print Jobs</p>
<p className="text-xl font-semibold text-white">{stats.completed_prints} <span className="text-sm font-normal text-bambu-gray">successful</span></p>
{stats.failed_prints > 0 && (
<p className="text-sm text-red-400">{stats.failed_prints} failed</p>
)}
{stats.total_archives - stats.completed_prints - stats.failed_prints > 0 && (
<p className="text-sm text-yellow-400">{stats.total_archives - stats.completed_prints - stats.failed_prints} in progress</p>
)}
</div>
</div>
</CardContent>
</Card>
<StatCard
icon={Clock}
label="Print Time"
@@ -1145,6 +1149,8 @@ export function ProjectDetailPage() {
...project,
archive_count: stats?.total_archives || 0,
total_items: stats?.total_items || 0,
completed_count: stats?.completed_prints || 0,
failed_count: stats?.failed_prints || 0,
queue_count: stats?.queued_prints || 0,
progress_percent: stats?.progress_percent || null,
archives: [],
+31 -14
View File
@@ -268,11 +268,11 @@ function ProjectCard({ project, onClick, onEdit, onDelete }: ProjectCardProps) {
? 'bg-bambu-green/20 text-bambu-green'
: 'bg-bambu-dark text-bambu-gray'
}`}>
{project.total_items}/{project.target_count} items
{project.completed_count}/{project.target_count} completed
</span>
) : project.total_items > 0 ? (
) : project.completed_count > 0 ? (
<span className="text-xs px-2 py-0.5 rounded-full whitespace-nowrap font-medium bg-bambu-dark text-bambu-gray">
{project.total_items} item{project.total_items !== 1 ? 's' : ''}
{project.completed_count} completed
</span>
) : null}
{isCompleted && (
@@ -377,7 +377,7 @@ function ProjectCard({ project, onClick, onEdit, onDelete }: ProjectCardProps) {
<div className="flex items-center justify-between text-xs mb-2">
<span className="text-bambu-gray">Progress</span>
<span className={progressPercent >= 100 ? 'text-bambu-green font-medium' : 'text-white'}>
{project.total_items} / {project.target_count}
{project.completed_count} / {project.target_count}
</span>
</div>
<div className="h-2.5 bg-bambu-dark/80 rounded-full overflow-hidden backdrop-blur-sm">
@@ -392,16 +392,27 @@ function ProjectCard({ project, onClick, onEdit, onDelete }: ProjectCardProps) {
}}
/>
</div>
<div className="text-right text-xs text-bambu-gray/60 mt-1">
{progressPercent.toFixed(0)}% complete
<div className="flex justify-between text-xs text-bambu-gray/60 mt-1">
<span>
{project.failed_count > 0 && `${project.failed_count} failed`}
</span>
<span>{progressPercent.toFixed(0)}% complete</span>
</div>
</>
) : project.total_items > 0 ? (
) : project.completed_count > 0 || project.failed_count > 0 ? (
<div className="flex items-center gap-4 text-xs">
<div className="flex items-center gap-1.5 text-bambu-gray">
<Archive className="w-3.5 h-3.5" />
<span>{project.total_items} item{project.total_items !== 1 ? 's' : ''} completed</span>
</div>
{project.completed_count > 0 && (
<div className="flex items-center gap-1.5 text-bambu-gray">
<Archive className="w-3.5 h-3.5" />
<span>{project.completed_count} completed</span>
</div>
)}
{project.failed_count > 0 && (
<div className="flex items-center gap-1.5 text-red-400">
<AlertTriangle className="w-3.5 h-3.5" />
<span>{project.failed_count} failed</span>
</div>
)}
{project.queue_count > 0 && (
<div className="flex items-center gap-1.5 text-blue-400">
<Clock className="w-3.5 h-3.5" />
@@ -456,10 +467,16 @@ function ProjectCard({ project, onClick, onEdit, onDelete }: ProjectCardProps) {
{/* Stats footer */}
<div className="flex items-center justify-between pt-3 border-t border-bambu-dark-tertiary">
<div className="flex items-center gap-4 text-xs text-bambu-gray">
<div className="flex items-center gap-1.5" title="Total items printed">
<Archive className="w-3.5 h-3.5" />
<span>{project.total_items}</span>
<div className="flex items-center gap-1.5" title="Completed prints">
<CheckCircle2 className="w-3.5 h-3.5 text-bambu-green" />
<span>{project.completed_count}</span>
</div>
{project.failed_count > 0 && (
<div className="flex items-center gap-1.5 text-red-400" title="Failed prints">
<AlertTriangle className="w-3.5 h-3.5" />
<span>{project.failed_count}</span>
</div>
)}
{project.queue_count > 0 && (
<div className="flex items-center gap-1.5 text-blue-400" title="In queue">
<ListTodo className="w-3.5 h-3.5" />