import type { AMSUnit, AMSTray } from '../../api/client'; import { getFillBarColor } from '../../utils/amsHelpers'; function trayColorToCSS(color: string | null): string { if (!color) return '#808080'; return `#${color.slice(0, 6)}`; } function isTrayEmpty(tray: AMSTray): boolean { return !tray.tray_type || tray.tray_type === ''; } function getAmsName(id: number): string { if (id <= 3) return `AMS ${String.fromCharCode(65 + id)}`; if (id >= 128 && id <= 135) return `AMS HT ${String.fromCharCode(65 + id - 128)}`; return `AMS ${id}`; } // --- SVG Icons (matching PrintersPage Bambu Lab style) --- function WaterDropEmpty({ className }: { className?: string }) { return ( ); } function WaterDropHalf({ className }: { className?: string }) { return ( ); } function WaterDropFull({ className }: { className?: string }) { return ( ); } function ThermometerEmpty({ className }: { className?: string }) { return ( ); } function ThermometerHalf({ className }: { className?: string }) { return ( ); } function ThermometerFull({ className }: { className?: string }) { return ( ); } // --- Threshold-colored indicators --- function HumidityIndicator({ humidity, goodThreshold = 40, fairThreshold = 60 }: { humidity: number; goodThreshold?: number; fairThreshold?: number }) { let textColor: string; let DropComponent: React.FC<{ className?: string }>; if (humidity <= goodThreshold) { textColor = '#22a352'; DropComponent = WaterDropEmpty; } else if (humidity <= fairThreshold) { textColor = '#d4a017'; DropComponent = WaterDropHalf; } else { textColor = '#c62828'; DropComponent = WaterDropFull; } return (
{humidity}%
); } function TemperatureIndicator({ temp, goodThreshold = 28, fairThreshold = 35 }: { temp: number; goodThreshold?: number; fairThreshold?: number }) { let textColor: string; let ThermoComponent: React.FC<{ className?: string }>; if (temp <= goodThreshold) { textColor = '#22a352'; ThermoComponent = ThermometerEmpty; } else if (temp <= fairThreshold) { textColor = '#d4a017'; ThermoComponent = ThermometerHalf; } else { textColor = '#c62828'; ThermoComponent = ThermometerFull; } return (
{temp}°C
); } // --- Nozzle badge --- function NozzleBadge({ side }: { side: 'L' | 'R' }) { return ( {side} ); } // --- Components --- interface SpoolSlotProps { tray: AMSTray; slotIndex: number; isActive: boolean; fillOverride?: number | null; spoolmanFill?: number | null; onClick?: () => void; } function SpoolSlot({ tray, slotIndex, isActive, fillOverride, spoolmanFill, onClick }: SpoolSlotProps) { const isEmpty = isTrayEmpty(tray); const color = trayColorToCSS(tray.tray_color); const amsFill = tray.remain !== null && tray.remain !== undefined && tray.remain >= 0 ? tray.remain : null; // If inventory says 0% but AMS reports positive remain, prefer AMS (#676) const resolvedOverride = (fillOverride === 0 && amsFill !== null && amsFill > 0) ? null : fillOverride; // Fill level fallback chain: Spoolman → Inventory → AMS remain const effectiveFill = spoolmanFill ?? resolvedOverride ?? amsFill; return (
{/* Spool visualization */}
{isEmpty ? (
) : ( )} {isActive && (
)}
{/* Material type */} {isEmpty ? 'Empty' : tray.tray_type || 'Unknown'} {/* Fill level bar */} {!isEmpty && effectiveFill !== null && effectiveFill >= 0 && (
)} {/* Slot number */} {slotIndex + 1}
); } export interface AmsThresholds { humidityGood: number; humidityFair: number; tempGood: number; tempFair: number; } interface AmsUnitCardProps { unit: AMSUnit; activeSlot: number | null; onConfigureSlot?: (amsId: number, trayId: number, tray: AMSTray | null) => void; isDualNozzle?: boolean; nozzleSide?: 'L' | 'R' | null; thresholds?: AmsThresholds; fillOverrides?: Record; spoolmanFillOverrides?: Record; } export function AmsUnitCard({ unit, activeSlot, onConfigureSlot, isDualNozzle, nozzleSide, thresholds, fillOverrides, spoolmanFillOverrides }: AmsUnitCardProps) { const trays = unit.tray || []; const isHt = unit.is_ams_ht; const slotCount = isHt ? 1 : 4; return (
{/* Header */}
{getAmsName(unit.id)} {isDualNozzle && nozzleSide && ( )}
{unit.temp != null && ( )} {unit.humidity != null && ( )}
{/* Slots grid */}
{Array.from({ length: slotCount }).map((_, i) => { const tray = trays[i] || { id: i, tray_color: null, tray_type: '', tray_sub_brands: null, tray_id_name: null, tray_info_idx: null, remain: -1, k: null, cali_idx: null, tag_uid: null, tray_uuid: null, nozzle_temp_min: null, nozzle_temp_max: null, }; return ( onConfigureSlot(unit.id, i, isTrayEmpty(tray) ? null : tray) : undefined} /> ); })}
); } // Exported for use in SpoolBuddyAmsPage compact cards export { HumidityIndicator, TemperatureIndicator, NozzleBadge };