import { useQuery } from '@tanstack/react-query';
import { useState, useEffect } from 'react';
import {
Package,
Clock,
CheckCircle,
XCircle,
DollarSign,
Printer,
Target,
Zap,
AlertTriangle,
TrendingDown,
FileSpreadsheet,
FileText,
Loader2,
Eye,
RotateCcw,
} from 'lucide-react';
import { Button } from '../components/Button';
import { useToast } from '../contexts/ToastContext';
import { api } from '../api/client';
import { PrintCalendar } from '../components/PrintCalendar';
import { FilamentTrends } from '../components/FilamentTrends';
import { Dashboard, type DashboardWidget } from '../components/Dashboard';
// Widget Components
function QuickStatsWidget({
stats,
currency,
}: {
stats: {
total_prints: number;
successful_prints: number;
failed_prints: number;
total_print_time_hours: number;
total_filament_grams: number;
total_cost: number;
total_energy_kwh: number;
total_energy_cost: number;
} | undefined;
currency: string;
}) {
return (
Total Prints
{stats?.total_prints || 0}
Print Time
{stats?.total_print_time_hours.toFixed(1) || 0}h
Filament Used
{((stats?.total_filament_grams || 0) / 1000).toFixed(2)}kg
Filament Cost
{currency} {stats?.total_cost.toFixed(2) || '0.00'}
Energy Used
{stats?.total_energy_kwh.toFixed(2) || '0.00'} kWh
Energy Cost
{currency} {stats?.total_energy_cost.toFixed(2) || '0.00'}
);
}
function SuccessRateWidget({
stats,
}: {
stats: {
total_prints: number;
successful_prints: number;
failed_prints: number;
} | undefined;
}) {
const successRate = stats?.total_prints
? Math.round((stats.successful_prints / stats.total_prints) * 100)
: 0;
return (
Successful:
{stats?.successful_prints || 0}
Failed:
{stats?.failed_prints || 0}
);
}
function TimeAccuracyWidget({
stats,
printerMap,
}: {
stats: {
average_time_accuracy: number | null;
time_accuracy_by_printer: Record | null;
} | undefined;
printerMap: Map;
}) {
const accuracy = stats?.average_time_accuracy;
if (accuracy === null || accuracy === undefined) {
return (
No time accuracy data yet
);
}
// Normalize accuracy for display (100% = perfect, clamp between 50-150 for gauge)
const displayValue = Math.min(150, Math.max(50, accuracy));
const normalizedForGauge = ((displayValue - 50) / 100) * 100; // 50-150 -> 0-100
// Color based on accuracy
const getColor = (acc: number) => {
if (acc >= 95 && acc <= 105) return '#00ae42'; // Green - within 5%
if (acc > 105) return '#3b82f6'; // Blue - faster than expected
return '#f97316'; // Orange - slower than expected
};
const color = getColor(accuracy);
const deviation = accuracy - 100;
return (
{accuracy.toFixed(0)}%
= 0 ? 'text-blue-400' : 'text-orange-400'}`}>
{deviation >= 0 ? '+' : ''}{deviation.toFixed(0)}%
100% = perfect estimate
{stats?.time_accuracy_by_printer && Object.keys(stats.time_accuracy_by_printer).length > 0 && (
{Object.entries(stats.time_accuracy_by_printer).slice(0, 3).map(([printerId, acc]) => (
{printerMap.get(printerId) || `Printer ${printerId}`}
= 95 && acc <= 105 ? 'text-bambu-green' :
acc > 105 ? 'text-blue-400' : 'text-orange-400'
}`}>
{acc.toFixed(0)}%
))}
)}
);
}
function FilamentTypesWidget({
stats,
}: {
stats: {
total_prints: number;
prints_by_filament_type: Record;
} | undefined;
}) {
if (!stats?.prints_by_filament_type || Object.keys(stats.prints_by_filament_type).length === 0) {
return No filament data available
;
}
// Sort by print count descending
const sortedEntries = Object.entries(stats.prints_by_filament_type).sort(
([, a], [, b]) => b - a
);
return (
{sortedEntries.map(([type, count]) => {
const percentage = Math.round((count / (stats.total_prints || 1)) * 100);
return (
);
})}
);
}
function PrintActivityWidget({ printDates }: { printDates: string[] }) {
return ;
}
function PrintsByPrinterWidget({
stats,
printerMap,
}: {
stats: { prints_by_printer: Record } | undefined;
printerMap: Map;
}) {
if (!stats?.prints_by_printer || Object.keys(stats.prints_by_printer).length === 0) {
return No printer data available
;
}
return (
{Object.entries(stats.prints_by_printer).map(([printerId, count]) => (
{printerMap.get(printerId) || `Printer ${printerId}`}
{count} prints
))}
);
}
function FilamentTrendsWidget({
archives,
currency,
}: {
archives: Parameters[0]['archives'];
currency: string;
}) {
if (!archives || archives.length === 0) {
return No print data available
;
}
return ;
}
function FailureAnalysisWidget() {
const { data: analysis, isLoading } = useQuery({
queryKey: ['failureAnalysis'],
queryFn: () => api.getFailureAnalysis({ days: 30 }),
});
if (isLoading) {
return (
);
}
if (!analysis || analysis.total_prints === 0) {
return No print data in the last 30 days
;
}
const topReasons = Object.entries(analysis.failures_by_reason)
.sort(([, a], [, b]) => b - a)
.slice(0, 5);
return (
{/* Summary */}
20 ? 'text-red-400' : analysis.failure_rate > 10 ? 'text-yellow-400' : 'text-bambu-green'}`} />
{analysis.failure_rate.toFixed(1)}%
failure rate
{analysis.failed_prints} / {analysis.total_prints} prints failed
{/* Top Failure Reasons */}
{topReasons.length > 0 && (
Top Failure Reasons
{topReasons.map(([reason, count]) => (
{reason || 'Unknown'}
{count}
))}
)}
{/* Trend indicator */}
{analysis.trend && analysis.trend.length >= 2 && (
Last week: {analysis.trend[analysis.trend.length - 1].failure_rate.toFixed(1)}%
)}
);
}
export function StatsPage() {
const { showToast } = useToast();
const [isExporting, setIsExporting] = useState(false);
const [showExportMenu, setShowExportMenu] = useState(false);
const [dashboardKey, setDashboardKey] = useState(0);
const [hiddenCount, setHiddenCount] = useState(0);
// Read hidden count from localStorage
useEffect(() => {
const updateHiddenCount = () => {
try {
const saved = localStorage.getItem('bambusy-dashboard-layout');
if (saved) {
const layout = JSON.parse(saved);
setHiddenCount(layout.hidden?.length || 0);
}
} catch {
setHiddenCount(0);
}
};
updateHiddenCount();
// Listen for storage changes
window.addEventListener('storage', updateHiddenCount);
// Also poll for changes (since storage event doesn't fire for same-tab changes)
const interval = setInterval(updateHiddenCount, 500);
return () => {
window.removeEventListener('storage', updateHiddenCount);
clearInterval(interval);
};
}, [dashboardKey]);
const { data: stats, isLoading } = useQuery({
queryKey: ['archiveStats'],
queryFn: api.getArchiveStats,
});
const { data: printers } = useQuery({
queryKey: ['printers'],
queryFn: api.getPrinters,
});
const { data: archives } = useQuery({
queryKey: ['archives'],
queryFn: () => api.getArchives(undefined, undefined, 1000, 0),
});
const { data: settings } = useQuery({
queryKey: ['settings'],
queryFn: api.getSettings,
});
const handleExport = async (format: 'csv' | 'xlsx') => {
setShowExportMenu(false);
setIsExporting(true);
try {
const { blob, filename } = await api.exportStats({ format, days: 90 });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
a.click();
URL.revokeObjectURL(url);
showToast('Export downloaded');
} catch (err) {
showToast('Export failed', 'error');
} finally {
setIsExporting(false);
}
};
const currency = settings?.currency || '$';
const printerMap = new Map(printers?.map((p) => [String(p.id), p.name]) || []);
const printDates = archives?.map((a) => a.created_at) || [];
if (isLoading) {
return (
);
}
// Define dashboard widgets
// Sizes: 1 = quarter (1/4), 2 = half (1/2), 4 = full width
const widgets: DashboardWidget[] = [
{
id: 'quick-stats',
title: 'Quick Stats',
component: ,
defaultSize: 2,
},
{
id: 'success-rate',
title: 'Success Rate',
component: ,
defaultSize: 1,
},
{
id: 'time-accuracy',
title: 'Time Accuracy',
component: ,
defaultSize: 1,
},
{
id: 'filament-types',
title: 'Filament Types',
component: ,
defaultSize: 1,
},
{
id: 'failure-analysis',
title: 'Failure Analysis (30 days)',
component: ,
defaultSize: 1,
},
{
id: 'print-activity',
title: 'Print Activity',
component: ,
defaultSize: 2,
},
{
id: 'prints-by-printer',
title: 'Prints by Printer',
component: ,
defaultSize: 2,
},
{
id: 'filament-trends',
title: 'Filament Usage Trends',
component: ,
defaultSize: 4,
},
];
return (
Dashboard
Drag widgets to rearrange. Click the eye icon to hide.
{/* Hidden widgets button - toggles panel in Dashboard */}
{hiddenCount > 0 && (
)}
{/* Reset Layout */}
{/* Export dropdown */}
{showExportMenu && (
)}
);
}