Преглед изворни кода

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
maziggy пре 4 месеци
родитељ
комит
c7377bfb0c
3 измењених фајлова са 59 додато и 34 уклоњено
  1. 3 1
      frontend/src/api/client.ts
  2. 25 19
      frontend/src/pages/ProjectDetailPage.tsx
  3. 31 14
      frontend/src/pages/ProjectsPage.tsx

+ 3 - 1
frontend/src/api/client.ts

@@ -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
frontend/src/pages/ProjectDetailPage.tsx

@@ -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
frontend/src/pages/ProjectsPage.tsx

@@ -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" />