import { useState, useMemo } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { Wrench, Loader2, Check, AlertTriangle, Clock, Plus, Trash2, ChevronDown, ChevronUp, Droplet, Flame, Ruler, Sparkles, Square, Cable, Edit3, RotateCcw, Calendar, Timer, Cog, Fan, Zap, Wind, Thermometer, Layers, Box, Target, RefreshCw, Settings, Filter, CircleDot, Printer, } from 'lucide-react'; import { api } from '../api/client'; import type { MaintenanceStatus, PrinterMaintenanceOverview, MaintenanceType } from '../api/client'; import { Card, CardContent } from '../components/Card'; import { Button } from '../components/Button'; import { Toggle } from '../components/Toggle'; import { useToast } from '../contexts/ToastContext'; // Icon mapping for maintenance types const iconMap: Record> = { Droplet, Flame, Ruler, Sparkles, Square, Cable, Wrench, Calendar, Timer, Cog, Fan, Zap, Wind, Thermometer, Layers, Box, Target, RefreshCw, Settings, Filter, CircleDot, }; function getIcon(iconName: string | null) { if (!iconName) return Wrench; return iconMap[iconName] || Wrench; } function formatDuration(value: number, type: 'hours' | 'days'): string { if (type === 'days') { if (value < 1) return 'Today'; if (value === 1) return '1 day'; if (value < 7) return `${Math.round(value)} days`; if (value < 30) return `${Math.round(value / 7)} weeks`; return `${Math.round(value / 30)} months`; } else { if (value < 1) return `${Math.round(value * 60)}m`; if (value < 10) return `${value.toFixed(1)}h`; return `${Math.round(value)}h`; } } function formatIntervalLabel(value: number, type: 'hours' | 'days'): string { if (type === 'days') { if (value === 1) return '1 day'; if (value === 7) return '1 week'; if (value === 14) return '2 weeks'; if (value === 30) return '1 month'; if (value === 60) return '2 months'; if (value === 90) return '3 months'; if (value === 180) return '6 months'; if (value === 365) return '1 year'; return `${value} days`; } return `${value}h`; } // Maintenance item card - cleaner, more visual design function MaintenanceCard({ item, onPerform, onToggle, }: { item: MaintenanceStatus; onPerform: (id: number) => void; onToggle: (id: number, enabled: boolean) => void; }) { const Icon = getIcon(item.maintenance_type_icon); const intervalType = item.interval_type || 'hours'; // Calculate progress based on interval type const getProgress = () => { if (intervalType === 'days') { const daysSince = item.days_since_maintenance ?? 0; return Math.max(0, Math.min(100, (daysSince / item.interval_hours) * 100)); } return Math.max(0, Math.min(100, ((item.interval_hours - item.hours_until_due) / item.interval_hours) * 100 )); }; const progressPercent = getProgress(); const getStatusColor = () => { if (!item.enabled) return 'text-bambu-gray'; if (item.is_due) return 'text-red-400'; if (item.is_warning) return 'text-amber-400'; return 'text-bambu-green'; }; const getProgressColor = () => { if (!item.enabled) return 'bg-bambu-gray/30'; if (item.is_due) return 'bg-red-500'; if (item.is_warning) return 'bg-amber-500'; return 'bg-bambu-green'; }; const getBgColor = () => { if (!item.enabled) return 'bg-bambu-dark-secondary/50'; if (item.is_due) return 'bg-red-500/5 border-red-500/20'; if (item.is_warning) return 'bg-amber-500/5 border-amber-500/20'; return 'bg-bambu-dark-secondary border-bambu-dark-tertiary'; }; const getStatusText = () => { if (!item.enabled) return 'Disabled'; if (intervalType === 'days') { const daysUntil = item.days_until_due ?? 0; if (item.is_due) return `Overdue by ${formatDuration(Math.abs(daysUntil), 'days')}`; if (item.is_warning) return `Due in ${formatDuration(daysUntil, 'days')}`; return `${formatDuration(daysUntil, 'days')} left`; } else { if (item.is_due) return `Overdue by ${formatDuration(Math.abs(item.hours_until_due), 'hours')}`; if (item.is_warning) return `Due in ${formatDuration(item.hours_until_due, 'hours')}`; return `${formatDuration(item.hours_until_due, 'hours')} left`; } }; return (
{/* Icon with status indicator */}
{item.enabled && (item.is_due || item.is_warning) && ( )}
{/* Content */}

{item.maintenance_type_name}

{intervalType === 'days' && ( )}
{/* Progress bar */}
{/* Status text */}
{item.is_due && } {item.is_warning && !item.is_due && } {!item.is_due && !item.is_warning && item.enabled && } {getStatusText()}
{/* Actions */}
onToggle(item.id, checked)} />
); } // Printer section with improved visual hierarchy function PrinterSection({ overview, onPerform, onToggle, onSetHours, }: { overview: PrinterMaintenanceOverview; onPerform: (id: number) => void; onToggle: (id: number, enabled: boolean) => void; onSetHours: (printerId: number, hours: number) => void; }) { const [expanded, setExpanded] = useState(true); const [editingHours, setEditingHours] = useState(false); const [hoursInput, setHoursInput] = useState(overview.total_print_hours.toFixed(1)); const sortedItems = [...overview.maintenance_items].sort((a, b) => { // Sort by urgency first, then by type if (a.is_due && !b.is_due) return -1; if (!a.is_due && b.is_due) return 1; if (a.is_warning && !b.is_warning) return -1; if (!a.is_warning && b.is_warning) return 1; return a.maintenance_type_id - b.maintenance_type_id; }); const nextTask = sortedItems.find(item => item.enabled && (item.is_due || item.is_warning)); const handleSaveHours = () => { const hours = parseFloat(hoursInput); if (!isNaN(hours) && hours >= 0) { onSetHours(overview.printer_id, hours); setEditingHours(false); } }; return ( {/* Header */}

{overview.printer_name}

{overview.due_count > 0 && ( {overview.due_count} overdue )} {overview.warning_count > 0 && ( {overview.warning_count} due soon )} {overview.due_count === 0 && overview.warning_count === 0 && ( All good )}
{/* Quick stats row */}
{/* Print Hours */}
{editingHours ? (
setHoursInput(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter') handleSaveHours(); if (e.key === 'Escape') setEditingHours(false); }} className="w-24 px-2 py-1 bg-bambu-dark border border-bambu-dark-tertiary rounded text-white text-sm" min="0" step="1" autoFocus /> hours
) : ( )}
{/* Divider */}
{/* Next Maintenance */} {nextTask && (
{(() => { const Icon = getIcon(nextTask.maintenance_type_icon); return ; })()}
{nextTask.maintenance_type_name}
{nextTask.is_due ? 'Overdue' : 'Due soon'}
)}
{/* Maintenance items */} {expanded && (
{sortedItems.map((item) => ( ))}
)} ); } // Settings section - maintenance types configuration function SettingsSection({ overview, types, onUpdateInterval, onAddType, onUpdateType, onDeleteType, onAssignType, onRemoveItem, }: { overview: PrinterMaintenanceOverview[] | undefined; types: MaintenanceType[]; onUpdateInterval: (id: number, data: { custom_interval_hours?: number | null; custom_interval_type?: 'hours' | 'days' | null }) => void; onAddType: (data: { name: string; description?: string; default_interval_hours: number; interval_type: 'hours' | 'days'; icon?: string }, printerIds: number[]) => void; onUpdateType: (id: number, data: { name?: string; default_interval_hours?: number; interval_type?: 'hours' | 'days'; icon?: string }) => void; onDeleteType: (id: number) => void; onAssignType: (printerId: number, typeId: number) => void; onRemoveItem: (itemId: number) => void; }) { const [editingInterval, setEditingInterval] = useState(null); const [intervalInput, setIntervalInput] = useState(''); const [intervalTypeInput, setIntervalTypeInput] = useState<'hours' | 'days'>('hours'); const [showAddType, setShowAddType] = useState(false); const [newTypeName, setNewTypeName] = useState(''); const [newTypeInterval, setNewTypeInterval] = useState('100'); const [newTypeIntervalType, setNewTypeIntervalType] = useState<'hours' | 'days'>('hours'); const [newTypeIcon, setNewTypeIcon] = useState('Wrench'); const [selectedPrinters, setSelectedPrinters] = useState>(new Set()); const [expandedType, setExpandedType] = useState(null); // Get unique printers from overview const printers = useMemo(() => { if (!overview) return []; return overview.map(o => ({ id: o.printer_id, name: o.printer_name })); }, [overview]); // Get which printers have a specific maintenance type assigned const getAssignedPrinters = (typeId: number) => { if (!overview) return []; return overview .filter(p => p.maintenance_items.some(item => item.maintenance_type_id === typeId)) .map(p => ({ printerId: p.printer_id, printerName: p.printer_name, itemId: p.maintenance_items.find(item => item.maintenance_type_id === typeId)?.id, })); }; // Get printers that DON'T have a specific type assigned const getUnassignedPrinters = (typeId: number) => { if (!overview) return []; const assignedIds = new Set(getAssignedPrinters(typeId).map(p => p.printerId)); return printers.filter(p => !assignedIds.has(p.id)); }; // Edit type state const [editingType, setEditingType] = useState(null); const [editTypeName, setEditTypeName] = useState(''); const [editTypeInterval, setEditTypeInterval] = useState(''); const [editTypeIntervalType, setEditTypeIntervalType] = useState<'hours' | 'days'>('hours'); const [editTypeIcon, setEditTypeIcon] = useState('Wrench'); const startEditType = (type: MaintenanceType) => { setEditingType(type); setEditTypeName(type.name); setEditTypeInterval(type.default_interval_hours.toString()); setEditTypeIntervalType(type.interval_type || 'hours'); setEditTypeIcon(type.icon || 'Wrench'); }; const handleSaveEditType = () => { if (editingType && editTypeName.trim() && parseFloat(editTypeInterval) > 0) { onUpdateType(editingType.id, { name: editTypeName.trim(), default_interval_hours: parseFloat(editTypeInterval), interval_type: editTypeIntervalType, icon: editTypeIcon, }); setEditingType(null); } }; const handleSaveInterval = (itemId: number, defaultInterval: number, defaultIntervalType: 'hours' | 'days') => { const newInterval = parseFloat(intervalInput); if (!isNaN(newInterval) && newInterval > 0) { const customInterval = Math.abs(newInterval - defaultInterval) < 0.01 ? null : newInterval; const customIntervalType = intervalTypeInput !== defaultIntervalType ? intervalTypeInput : null; onUpdateInterval(itemId, { custom_interval_hours: customInterval, custom_interval_type: customIntervalType }); } setEditingInterval(null); }; const handleAddType = (e: React.FormEvent) => { e.preventDefault(); if (newTypeName.trim() && parseFloat(newTypeInterval) > 0 && selectedPrinters.size > 0) { onAddType({ name: newTypeName.trim(), default_interval_hours: parseFloat(newTypeInterval), interval_type: newTypeIntervalType, icon: newTypeIcon, }, Array.from(selectedPrinters)); setNewTypeName(''); setNewTypeInterval('100'); setNewTypeIntervalType('hours'); setSelectedPrinters(new Set()); setShowAddType(false); } }; const togglePrinterSelection = (printerId: number) => { setSelectedPrinters(prev => { const next = new Set(prev); if (next.has(printerId)) { next.delete(printerId); } else { next.add(printerId); } return next; }); }; const printerItems = overview?.map(p => ({ printerId: p.printer_id, printerName: p.printer_name, items: p.maintenance_items.sort((a, b) => a.maintenance_type_id - b.maintenance_type_id), })).sort((a, b) => a.printerName.localeCompare(b.printerName)) || []; const systemTypes = types.filter(t => t.is_system); const customTypes = types.filter(t => !t.is_system); return (
{/* Maintenance Types */}

Maintenance Types

System types and your custom maintenance tasks

{/* Add custom type form */} {showAddType && (
setNewTypeName(e.target.value)} className="w-full px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white text-sm focus:border-bambu-green focus:outline-none" placeholder="e.g., Replace HEPA Filter" autoFocus />
setNewTypeInterval(e.target.value)} className="w-full px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white text-sm focus:border-bambu-green focus:outline-none" min="1" />
{Object.keys(iconMap).map((iconName) => { const IconComp = iconMap[iconName]; return ( ); })}
{/* Printer selection */}
{printers.map(p => ( ))}
{selectedPrinters.size === 0 && (

Select at least one printer

)}
)} {/* Types grid */}
{/* System types */} {systemTypes.map((type) => { const Icon = getIcon(type.icon); const intervalType = type.interval_type || 'hours'; return (
{type.name}
{intervalType === 'days' ? : } {formatIntervalLabel(type.default_interval_hours, intervalType)}
); })} {/* Custom types */} {customTypes.map((type) => { const Icon = getIcon(type.icon); const intervalType = type.interval_type || 'hours'; const isEditing = editingType?.id === type.id; if (isEditing) { return (
setEditTypeName(e.target.value)} className="w-full px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white text-sm focus:border-bambu-green focus:outline-none" placeholder="Name" autoFocus />
setEditTypeInterval(e.target.value)} className="w-24 px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white text-sm focus:border-bambu-green focus:outline-none" min="1" />
{Object.keys(iconMap).map((iconName) => { const IconComp = iconMap[iconName]; return ( ); })}
); } const assignedPrinters = getAssignedPrinters(type.id); const unassignedPrinters = getUnassignedPrinters(type.id); const isExpanded = expandedType === type.id; return (
{type.name} Custom
{intervalType === 'days' ? : } {formatIntervalLabel(type.default_interval_hours, intervalType)}
{/* Printer assignment management */} {isExpanded && (

Assigned to printers:

{assignedPrinters.length === 0 ? (

No printers assigned

) : (
{assignedPrinters.map(p => ( {p.printerName} ))}
)} {unassignedPrinters.length > 0 && (
Add: {unassignedPrinters.map(p => ( ))}
)}
)}
); })}
{/* Per-printer interval overrides */} {printerItems.length > 0 && (

Interval Overrides

Customize intervals for specific printers

{printerItems.map((printer) => (

{printer.printerName}

{printer.items.map((item) => { const Icon = getIcon(item.maintenance_type_icon); const typeInfo = types.find(t => t.id === item.maintenance_type_id); const defaultInterval = typeInfo?.default_interval_hours || item.interval_hours; const defaultIntervalType = typeInfo?.interval_type || 'hours'; const intervalType = item.interval_type || 'hours'; const isEditing = editingInterval === item.id; return (
{item.maintenance_type_name} {isEditing ? (
{intervalTypeInput === 'days' ? ( ) : ( )} setIntervalInput(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter') handleSaveInterval(item.id, defaultInterval, defaultIntervalType); if (e.key === 'Escape') setEditingInterval(null); }} className="w-16 px-2 py-1 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded text-white text-xs" min="1" />
) : ( )}
); })}
))}
)} {printerItems.length === 0 && (

No printers configured

Add printers to configure maintenance intervals

)}
); } type TabType = 'status' | 'settings'; export function MaintenancePage() { const queryClient = useQueryClient(); const { showToast } = useToast(); const [activeTab, setActiveTab] = useState('status'); const { data: overview, isLoading } = useQuery({ queryKey: ['maintenanceOverview'], queryFn: api.getMaintenanceOverview, }); const { data: types } = useQuery({ queryKey: ['maintenanceTypes'], queryFn: api.getMaintenanceTypes, }); const performMutation = useMutation({ mutationFn: ({ id, notes }: { id: number; notes?: string }) => api.performMaintenance(id, notes), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['maintenanceOverview'] }); queryClient.invalidateQueries({ queryKey: ['maintenanceSummary'] }); showToast('Maintenance marked as complete'); }, onError: (error: Error) => { showToast(error.message, 'error'); }, }); const updateMutation = useMutation({ mutationFn: ({ id, data }: { id: number; data: { custom_interval_hours?: number | null; custom_interval_type?: 'hours' | 'days' | null; enabled?: boolean } }) => api.updateMaintenanceItem(id, data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['maintenanceOverview'] }); }, onError: (error: Error) => { showToast(error.message, 'error'); }, }); // addTypeMutation removed - we now handle type creation with printer assignment // directly in onAddType callback const updateTypeMutation = useMutation({ mutationFn: ({ id, data }: { id: number; data: Partial<{ name: string; default_interval_hours: number; interval_type: 'hours' | 'days'; icon: string }> }) => api.updateMaintenanceType(id, data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['maintenanceTypes'] }); queryClient.invalidateQueries({ queryKey: ['maintenanceOverview'] }); showToast('Maintenance type updated'); }, onError: (error: Error) => { showToast(error.message, 'error'); }, }); const deleteTypeMutation = useMutation({ mutationFn: api.deleteMaintenanceType, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['maintenanceTypes'] }); queryClient.invalidateQueries({ queryKey: ['maintenanceOverview'] }); showToast('Maintenance type deleted'); }, onError: (error: Error) => { showToast(error.message, 'error'); }, }); const setHoursMutation = useMutation({ mutationFn: ({ printerId, hours }: { printerId: number; hours: number }) => api.setPrinterHours(printerId, hours), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['maintenanceOverview'] }); queryClient.invalidateQueries({ queryKey: ['maintenanceSummary'] }); showToast('Print hours updated'); }, onError: (error: Error) => { showToast(error.message, 'error'); }, }); const assignTypeMutation = useMutation({ mutationFn: ({ printerId, typeId }: { printerId: number; typeId: number }) => api.assignMaintenanceType(printerId, typeId), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['maintenanceOverview'] }); showToast('Printer assigned'); }, onError: (error: Error) => { showToast(error.message, 'error'); }, }); const removeItemMutation = useMutation({ mutationFn: api.removeMaintenanceItem, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['maintenanceOverview'] }); showToast('Printer removed'); }, onError: (error: Error) => { showToast(error.message, 'error'); }, }); const handlePerform = (id: number) => { performMutation.mutate({ id }); }; const handleToggle = (id: number, enabled: boolean) => { updateMutation.mutate({ id, data: { enabled } }); }; const handleSetHours = (printerId: number, hours: number) => { setHoursMutation.mutate({ printerId, hours }); }; if (isLoading) { return (
); } const totalDue = overview?.reduce((sum, p) => sum + p.due_count, 0) || 0; const totalWarning = overview?.reduce((sum, p) => sum + p.warning_count, 0) || 0; return (
{/* Header */}

Maintenance

{activeTab === 'status' ? ( <> {totalDue > 0 && {totalDue} task{totalDue !== 1 ? 's' : ''} overdue} {totalDue > 0 && totalWarning > 0 && ' · '} {totalWarning > 0 && {totalWarning} due soon} {totalDue === 0 && totalWarning === 0 && All maintenance up to date} ) : ( 'Configure maintenance types and intervals' )}

{/* Tabs */}
{/* Tab content */} {activeTab === 'status' ? (
{overview && overview.length > 0 ? ( [...overview].sort((a, b) => { // Sort printers with issues first const aScore = a.due_count * 10 + a.warning_count; const bScore = b.due_count * 10 + b.warning_count; if (aScore !== bScore) return bScore - aScore; return a.printer_name.localeCompare(b.printer_name); }).map((printerOverview) => ( )) ) : (

No printers configured

Add printers to start tracking maintenance

)}
) : ( updateMutation.mutate({ id, data }) } onAddType={async (data, printerIds) => { // Create the type first, then assign to selected printers const newType = await api.createMaintenanceType(data); // Assign to each selected printer for (const printerId of printerIds) { await api.assignMaintenanceType(printerId, newType.id); } queryClient.invalidateQueries({ queryKey: ['maintenanceTypes'] }); queryClient.invalidateQueries({ queryKey: ['maintenanceOverview'] }); showToast('Maintenance type added'); }} onUpdateType={(id, data) => updateTypeMutation.mutate({ id, data })} onDeleteType={(id) => deleteTypeMutation.mutate(id)} onAssignType={(printerId, typeId) => assignTypeMutation.mutate({ printerId, typeId })} onRemoveItem={(itemId) => removeItemMutation.mutate(itemId)} /> )}
); }