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 (
{successRate}%
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 (
{type} {count} prints
); })}
); } 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 (
Loading statistics...
); } // 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 && (
)}
); }