diff --git a/frontend/src/components/spoolbuddy/AmsUnitCard.tsx b/frontend/src/components/spoolbuddy/AmsUnitCard.tsx index 7cade8c84..818fa5d59 100644 --- a/frontend/src/components/spoolbuddy/AmsUnitCard.tsx +++ b/frontend/src/components/spoolbuddy/AmsUnitCard.tsx @@ -1,4 +1,3 @@ -import { useTranslation } from 'react-i18next'; import type { AMSUnit, AMSTray } from '../../api/client'; function trayColorToCSS(color: string | null): string { @@ -10,6 +9,18 @@ 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}`; +} + +function formatHumidity(value: number | null): string { + if (value === null || value === undefined) return '-'; + if (value > 5) return `${value}%`; + return `Level ${value}`; +} + interface SpoolSlotProps { tray: AMSTray; slotIndex: number; @@ -21,47 +32,47 @@ function SpoolSlot({ tray, slotIndex, isActive }: SpoolSlotProps) { const color = trayColorToCSS(tray.tray_color); return ( -
+
{/* Spool visualization */}
{isEmpty ? ( -
-
+
+
) : ( - - + + )} {isActive && ( -
+
)}
{/* Material type */} - + {isEmpty ? 'Empty' : tray.tray_type || 'Unknown'} {/* Fill level bar */} {!isEmpty && tray.remain !== null && tray.remain !== undefined && tray.remain >= 0 && ( -
+
50 ? '#22c55e' : tray.remain > 15 ? '#f59e0b' : '#ef4444', + backgroundColor: tray.remain > 50 ? '#22c55e' : tray.remain > 20 ? '#f59e0b' : '#ef4444', }} />
)} {/* Slot number */} - {slotIndex + 1} + {slotIndex + 1}
); } @@ -72,29 +83,21 @@ interface AmsUnitCardProps { } export function AmsUnitCard({ unit, activeSlot }: AmsUnitCardProps) { - const { t } = useTranslation(); const trays = unit.tray || []; const isHt = unit.is_ams_ht; - - const 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}`; - }; - const slotCount = isHt ? 1 : 4; return ( -
+
{/* Header */}
{getAmsName(unit.id)} {unit.humidity !== null && unit.humidity !== undefined && ( -
+
- {unit.humidity > 5 ? `${unit.humidity}%` : `${t('spoolbuddy.ams.level', 'Level')} ${unit.humidity}`} + {formatHumidity(unit.humidity)}
)}
diff --git a/frontend/src/components/spoolbuddy/LinkSpoolModal.tsx b/frontend/src/components/spoolbuddy/LinkSpoolModal.tsx new file mode 100644 index 000000000..d9e177cfa --- /dev/null +++ b/frontend/src/components/spoolbuddy/LinkSpoolModal.tsx @@ -0,0 +1,144 @@ +import { useState, useEffect, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; +import { X } from 'lucide-react'; +import type { InventorySpool } from '../../api/client'; +import { SpoolIcon } from './SpoolIcon'; + +interface LinkSpoolModalProps { + isOpen: boolean; + onClose: () => void; + tagId: string; + untaggedSpools: InventorySpool[]; + onLink: (spool: InventorySpool) => void; +} + +export function LinkSpoolModal({ + isOpen, + onClose, + tagId, + untaggedSpools, + onLink, +}: LinkSpoolModalProps) { + const { t } = useTranslation(); + const [selectedSpool, setSelectedSpool] = useState(null); + + const handleClose = useCallback(() => { + setSelectedSpool(null); + onClose(); + }, [onClose]); + + // Handle escape key + const handleKeyDown = useCallback((e: KeyboardEvent) => { + if (e.key === 'Escape') { + handleClose(); + } + }, [handleClose]); + + useEffect(() => { + if (isOpen) { + document.addEventListener('keydown', handleKeyDown); + document.body.style.overflow = 'hidden'; + } + return () => { + document.removeEventListener('keydown', handleKeyDown); + document.body.style.overflow = ''; + }; + }, [isOpen, handleKeyDown]); + + if (!isOpen) return null; + + const handleConfirm = () => { + if (selectedSpool) { + onLink(selectedSpool); + setSelectedSpool(null); + } + }; + + return ( +
+
e.stopPropagation()} + > + {/* Header */} +
+
+

+ {t('spoolbuddy.dashboard.linkTagTitle', 'Link Tag to Spool')} +

+

{tagId}

+
+ +
+ + {/* Content */} +
+

+ {t('spoolbuddy.dashboard.selectSpool', 'Select a spool to link this tag to:')} +

+ + {untaggedSpools.length === 0 ? ( +
+ {t('spoolbuddy.dashboard.noUntagged', 'No spools without tags found')} +
+ ) : ( +
+ {untaggedSpools.map((spool) => ( + + ))} +
+ )} +
+ + {/* Footer */} +
+ + +
+
+
+ ); +} diff --git a/frontend/src/components/spoolbuddy/SpoolBuddyBottomNav.tsx b/frontend/src/components/spoolbuddy/SpoolBuddyBottomNav.tsx index 74205f705..80701f82b 100644 --- a/frontend/src/components/spoolbuddy/SpoolBuddyBottomNav.tsx +++ b/frontend/src/components/spoolbuddy/SpoolBuddyBottomNav.tsx @@ -49,7 +49,7 @@ export function SpoolBuddyBottomNav() { const { t } = useTranslation(); return ( -