import { useState, useMemo } from 'react'; import { useQueryClient } from '@tanstack/react-query'; import { Printer as PrinterIcon, Loader2, AlertCircle, AlertTriangle, Check, Circle, RefreshCw, Wand2, Users, } from 'lucide-react'; import { api } from '../../api/client'; import { getColorName } from '../../utils/colors'; import { normalizeColorForCompare, colorsAreSimilar, } from '../../utils/amsHelpers'; import type { PrinterSelectorProps, AssignmentMode } from './types'; import type { PrinterMappingResult, PerPrinterConfig } from '../../hooks/useMultiPrinterFilamentMapping'; import type { FilamentRequirement, LoadedFilament } from '../../hooks/useFilamentMapping'; interface PrinterSelectorWithMappingProps extends PrinterSelectorProps { /** Per-printer mapping results (only used when multiple printers selected) */ printerMappingResults?: PrinterMappingResult[]; /** Filament requirements for the print */ filamentReqs?: { filaments: FilamentRequirement[] }; /** Callback to auto-configure a printer */ onAutoConfigurePrinter?: (printerId: number) => void; /** Callback to update printer config */ onUpdatePrinterConfig?: (printerId: number, config: Partial) => void; /** Current assignment mode */ assignmentMode?: AssignmentMode; /** Handler for assignment mode change */ onAssignmentModeChange?: (mode: AssignmentMode) => void; /** Selected target model (when assignmentMode is 'model') */ targetModel?: string | null; /** Handler for target model change */ onTargetModelChange?: (model: string | null) => void; /** Suggested model from sliced file (for pre-selection) */ slicedForModel?: string | null; } /** * Inline AMS mapping editor for a single printer. */ function InlineMappingEditor({ printerResult, filamentReqs, onUpdateConfig, }: { printerResult: PrinterMappingResult; filamentReqs: FilamentRequirement[]; onUpdateConfig: (config: Partial) => void; }) { const queryClient = useQueryClient(); const [isRefreshing, setIsRefreshing] = useState(false); const handleSlotChange = (slotId: number, value: string) => { if (slotId <= 0) return; const newMappings = { ...printerResult.config.manualMappings }; if (value === '') { delete newMappings[slotId]; } else { newMappings[slotId] = parseInt(value, 10); } onUpdateConfig({ useDefault: false, manualMappings: newMappings, autoConfigured: false, }); }; const handleRefresh = async () => { setIsRefreshing(true); try { await api.refreshPrinterStatus(printerResult.printerId); await new Promise((r) => setTimeout(r, 500)); await queryClient.refetchQueries({ queryKey: ['printer-status', printerResult.printerId] }); } finally { setIsRefreshing(false); } }; // Compute current slot assignments const slotAssignments = filamentReqs.map((req) => { const slotId = req.slot_id || 0; const currentMapping = printerResult.config.manualMappings[slotId]; let loaded: LoadedFilament | undefined; let isManual = false; if (currentMapping !== undefined) { loaded = printerResult.loadedFilaments.find((f) => f.globalTrayId === currentMapping); isManual = true; } else { // Auto-match logic const usedTrayIds = new Set(Object.values(printerResult.config.manualMappings)); const exactMatch = printerResult.loadedFilaments.find( (f) => !usedTrayIds.has(f.globalTrayId) && f.type?.toUpperCase() === req.type?.toUpperCase() && normalizeColorForCompare(f.color) === normalizeColorForCompare(req.color) ); const similarMatch = exactMatch ? undefined : printerResult.loadedFilaments.find( (f) => !usedTrayIds.has(f.globalTrayId) && f.type?.toUpperCase() === req.type?.toUpperCase() && colorsAreSimilar(f.color, req.color) ); const typeOnlyMatch = exactMatch || similarMatch ? undefined : printerResult.loadedFilaments.find( (f) => !usedTrayIds.has(f.globalTrayId) && f.type?.toUpperCase() === req.type?.toUpperCase() ); loaded = exactMatch ?? similarMatch ?? typeOnlyMatch; } // Determine status let status: 'match' | 'type_only' | 'mismatch' = 'mismatch'; if (loaded) { const typeMatch = loaded.type?.toUpperCase() === req.type?.toUpperCase(); const colorMatch = normalizeColorForCompare(loaded.color) === normalizeColorForCompare(req.color) || colorsAreSimilar(loaded.color, req.color); if (typeMatch && colorMatch) { status = 'match'; } else if (typeMatch) { status = 'type_only'; } } return { req, loaded, status, isManual }; }); return (
Custom slot mapping
{slotAssignments.map(({ req, loaded, status, isManual }, idx) => (
{req.type} ({req.used_grams}g) {status === 'match' ? ( ) : status === 'type_only' ? ( ) : ( )}
))}
); } /** * Printer selection component with grid-based UI. * Supports single or multi-select modes. * When multiple printers are selected, shows per-printer mapping overrides. */ export function PrinterSelector({ printers, selectedPrinterIds, onMultiSelect, isLoading = false, allowMultiple = false, showInactive = false, printerMappingResults, filamentReqs, onAutoConfigurePrinter, onUpdatePrinterConfig, assignmentMode = 'printer', onAssignmentModeChange, targetModel, onTargetModelChange, slicedForModel, }: PrinterSelectorWithMappingProps) { // State for showing all printers vs only matching model const [showAllPrinters, setShowAllPrinters] = useState(false); // Filter printers based on showInactive flag const activePrinters = showInactive ? printers : printers.filter((p) => p.is_active); // Filter by sliced model (only in printer mode, when slicedForModel is set) const displayPrinters = useMemo(() => { if (assignmentMode !== 'printer' || !slicedForModel || showAllPrinters) { return activePrinters; } // Filter to only show printers matching the sliced model const matching = activePrinters.filter((p) => p.model === slicedForModel); // If no matching printers, show all return matching.length > 0 ? matching : activePrinters; }, [activePrinters, assignmentMode, slicedForModel, showAllPrinters]); // Check if there are hidden printers due to model filtering const hiddenPrinterCount = activePrinters.length - displayPrinters.length; // Get unique models from available printers (for model-based assignment) const uniqueModels = useMemo(() => { const models = activePrinters .map(p => p.model) .filter((m): m is string => Boolean(m)); return [...new Set(models)].sort(); }, [activePrinters]); // Check if model-based assignment is available (need callbacks and multiple printers of same model) const modelAssignmentAvailable = onAssignmentModeChange && onTargetModelChange && uniqueModels.length > 0; const showMappingOptions = allowMultiple && selectedPrinterIds.length > 1 && printerMappingResults && filamentReqs?.filaments && filamentReqs.filaments.length > 0 && onAutoConfigurePrinter && onUpdatePrinterConfig; if (isLoading) { return (
); } if (displayPrinters.length === 0) { return (
No {showInactive ? '' : 'active '}printers available
); } const handlePrinterClick = (printerId: number) => { if (allowMultiple) { if (selectedPrinterIds.includes(printerId)) { onMultiSelect(selectedPrinterIds.filter((id) => id !== printerId)); } else { onMultiSelect([...selectedPrinterIds, printerId]); } } else { onMultiSelect([printerId]); } }; const handleSelectAll = () => { onMultiSelect(displayPrinters.map((p) => p.id)); }; const handleDeselectAll = () => { onMultiSelect([]); }; const handleOverrideToggle = (printerId: number, enabled: boolean, e: React.MouseEvent) => { e.stopPropagation(); if (!onAutoConfigurePrinter || !onUpdatePrinterConfig) return; if (enabled) { onAutoConfigurePrinter(printerId); } else { onUpdatePrinterConfig(printerId, { useDefault: true, manualMappings: {}, autoConfigured: false, }); } }; const isSelected = (printerId: number) => selectedPrinterIds.includes(printerId); const selectedCount = selectedPrinterIds.length; const getPrinterMappingResult = (printerId: number) => { return printerMappingResults?.find((r) => r.printerId === printerId); }; return (
{/* Assignment mode toggle (model vs specific printer) */} {modelAssignmentAvailable && (
)} {/* Model info (when in model mode) */} {assignmentMode === 'model' && modelAssignmentAvailable && targetModel && (

Scheduler will assign to first available idle {targetModel} printer

)} {/* Multi-select header (only in printer mode) */} {assignmentMode === 'printer' && allowMultiple && displayPrinters.length > 1 && (
{selectedCount === 0 ? 'Select printers' : `${selectedCount} printer${selectedCount !== 1 ? 's' : ''} selected`}
{selectedCount < displayPrinters.length && ( )} {selectedCount > 0 && ( )}
)} {/* Printer list (only in printer mode) */} {assignmentMode === 'printer' && displayPrinters.map((printer) => { const selected = isSelected(printer.id); const mappingResult = getPrinterMappingResult(printer.id); const hasOverride = mappingResult && !mappingResult.config.useDefault; return (
{/* Printer selection button */} {/* Per-printer override checkbox + mapping (only when selected and multi-printer) */} {selected && showMappingOptions && mappingResult && (
{/* Override checkbox row */}
{/* Match status indicator */} ({mappingResult.exactMatches}/{mappingResult.totalSlots} matched) {/* Loading indicator */} {mappingResult.isLoading && ( )} {/* Auto-configure button (when override is enabled) */} {hasOverride && ( )}
{/* Inline mapping editor (shown when override is checked) */} {hasOverride && ( onUpdatePrinterConfig!(printer.id, config)} /> )}
)}
); })} {/* Show hidden printers toggle */} {assignmentMode === 'printer' && hiddenPrinterCount > 0 && !showAllPrinters && ( )} {/* Show matching only toggle */} {assignmentMode === 'printer' && showAllPrinters && slicedForModel && ( )} {/* Warning when no printer selected (only in printer mode) */} {assignmentMode === 'printer' && selectedCount === 0 && (

Select at least one printer

)} {/* Warning when no model selected (only in model mode) */} {assignmentMode === 'model' && !targetModel && (

Select a target printer model

)}
); }