import { useState } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useTranslation } from 'react-i18next'; import { X, Loader2, Package, Check, Search } from 'lucide-react'; import { api } from '../api/client'; import type { InventorySpool, SpoolAssignment } from '../api/client'; import { Button } from './Button'; import { useToast } from '../contexts/ToastContext'; interface AssignSpoolModalProps { isOpen: boolean; onClose: () => void; printerId: number; amsId: number; trayId: number; trayInfo?: { type: string; color: string; location: string; }; } export function AssignSpoolModal({ isOpen, onClose, printerId, amsId, trayId, trayInfo }: AssignSpoolModalProps) { const { t } = useTranslation(); const queryClient = useQueryClient(); const { showToast } = useToast(); const [selectedSpoolId, setSelectedSpoolId] = useState(null); const [searchFilter, setSearchFilter] = useState(''); const { data: spools, isLoading } = useQuery({ queryKey: ['inventory-spools'], queryFn: () => api.getSpools(), enabled: isOpen, }); const { data: assignments } = useQuery({ queryKey: ['spool-assignments'], queryFn: () => api.getAssignments(), enabled: isOpen, }); const assignMutation = useMutation({ mutationFn: (spoolId: number) => api.assignSpool({ spool_id: spoolId, printer_id: printerId, ams_id: amsId, tray_id: trayId }), onSuccess: (newAssignment) => { // Immediately update cache so UI reflects the new assignment without waiting for refetch queryClient.setQueryData(['spool-assignments'], (old) => { const filtered = (old || []).filter(a => !(a.printer_id === printerId && a.ams_id === amsId && a.tray_id === trayId) ); filtered.push(newAssignment); return filtered; }); queryClient.invalidateQueries({ queryKey: ['spool-assignments'] }); showToast(t('inventory.assignSuccess'), 'success'); onClose(); }, onError: (error: Error) => { showToast(`${t('inventory.assignFailed')}: ${error.message}`, 'error'); }, }); if (!isOpen) return null; // Filter out Bambu Lab spools (identified by RFID tag_uid or tray_uuid) // and spools already assigned to other slots const assignedSpoolIds = new Set( (assignments || []) .filter(a => !(a.printer_id === printerId && a.ams_id === amsId && a.tray_id === trayId)) .map(a => a.spool_id) ); const manualSpools = spools?.filter((spool: InventorySpool) => !spool.tag_uid && !spool.tray_uuid && !assignedSpoolIds.has(spool.id) ); const filteredSpools = manualSpools?.filter((spool: InventorySpool) => { if (!searchFilter) return true; const q = searchFilter.toLowerCase(); return ( spool.material.toLowerCase().includes(q) || (spool.brand?.toLowerCase().includes(q) ?? false) || (spool.color_name?.toLowerCase().includes(q) ?? false) || (spool.subtype?.toLowerCase().includes(q) ?? false) ); }); const handleAssign = () => { if (selectedSpoolId) { assignMutation.mutate(selectedSpoolId); } }; return (
{/* Header */}

{t('inventory.assignSpool')}

{/* Content */}
{/* Tray info */} {trayInfo && (

{t('inventory.selectSpool')}:

{trayInfo.color && ( )} {trayInfo.type || t('ams.emptySlot')} ({trayInfo.location})
)} {/* Search filter */}
setSearchFilter(e.target.value)} placeholder={t('inventory.searchSpools')} className="w-full pl-9 pr-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white text-sm placeholder:text-bambu-gray focus:outline-none focus:border-bambu-green" />
{/* Spool list */}
{isLoading ? (
) : filteredSpools && filteredSpools.length > 0 ? (
{filteredSpools.map((spool: InventorySpool) => ( ))}
) : manualSpools && manualSpools.length === 0 ? (

{t('inventory.noManualSpools')}

) : (

{t('inventory.noSpoolsMatch')}

)}
{/* Footer */}
{assignMutation.isError && (
{(assignMutation.error as Error).message}
)}
); }