import { useState, useMemo } from 'react'; import { useQuery } from '@tanstack/react-query'; import { useTranslation } from 'react-i18next'; import { Search, X, Package } from 'lucide-react'; import { api } from '../../api/client'; import type { InventorySpool, SpoolAssignment } from '../../api/client'; import { resolveSpoolColorName } from '../../utils/colors'; import { formatSlotLabel } from '../../utils/amsHelpers'; type FilterMode = 'all' | 'in_ams' | string; // string = material name function spoolColor(spool: InventorySpool): string { if (spool.rgba) return `#${spool.rgba.substring(0, 6)}`; return '#808080'; } function spoolRemaining(spool: InventorySpool): number { return Math.max(0, spool.label_weight - spool.weight_used); } function spoolPct(spool: InventorySpool): number { if (spool.label_weight <= 0) return 0; return Math.max(0, Math.min(100, ((spool.label_weight - spool.weight_used) / spool.label_weight) * 100)); } function spoolDisplayName(spool: InventorySpool): string { const parts = [spool.material]; if (spool.subtype) parts.push(spool.subtype); return parts.join(' '); } function assignmentLabel(a: SpoolAssignment): string { const isExternal = a.ams_id === 254 || a.ams_id === 255; const isHt = !isExternal && a.ams_id >= 128; return formatSlotLabel(a.ams_id, a.tray_id, isHt, isExternal); } /* Spool circle — same style as AMS page tray slots */ function SpoolCircle({ color, size = 56 }: { color: string; size?: number }) { return ( ); } export function SpoolBuddyInventoryPage() { const { t } = useTranslation(); const [searchQuery, setSearchQuery] = useState(''); const [filterMode, setFilterMode] = useState('all'); const [selectedSpoolId, setSelectedSpoolId] = useState(null); const { data: spoolmanSettings } = useQuery({ queryKey: ['spoolman-settings'], queryFn: api.getSpoolmanSettings, staleTime: 5 * 60 * 1000, }); const { data: spools = [], isLoading } = useQuery({ queryKey: ['inventory-spools'], queryFn: () => api.getSpools(false), refetchInterval: 30000, }); const { data: assignments = [] } = useQuery({ queryKey: ['spool-assignments'], queryFn: () => api.getAssignments(), refetchInterval: 30000, }); // Build assignment lookup: spool_id → assignment const assignmentMap = useMemo(() => { const map: Record = {}; assignments.forEach(a => { map[a.spool_id] = a; }); return map; }, [assignments]); const activeSpools = useMemo(() => spools.filter(s => !s.archived_at), [spools]); // Spools that have an AMS assignment const assignedSpoolIds = useMemo(() => new Set(assignments.map(a => a.spool_id)), [assignments]); const inAmsCount = useMemo(() => activeSpools.filter(s => assignedSpoolIds.has(s.id)).length, [activeSpools, assignedSpoolIds]); // Unique materials for filter pills const materials = useMemo(() => { const set = new Set(); activeSpools.forEach(s => set.add(s.material)); return Array.from(set).sort(); }, [activeSpools]); // Filter and sort const filteredSpools = useMemo(() => { let list = activeSpools; if (filterMode === 'in_ams') { list = list.filter(s => assignedSpoolIds.has(s.id)); } else if (filterMode !== 'all') { list = list.filter(s => s.material === filterMode); } if (searchQuery.trim()) { const q = searchQuery.toLowerCase().trim(); list = list.filter(s => s.material.toLowerCase().includes(q) || (s.subtype && s.subtype.toLowerCase().includes(q)) || (s.brand && s.brand.toLowerCase().includes(q)) || (s.color_name && s.color_name.toLowerCase().includes(q)) || (s.note && s.note.toLowerCase().includes(q)) ); } // Sort: assigned spools first (by slot label), then by most recently updated return [...list].sort((a, b) => { const aAssigned = assignedSpoolIds.has(a.id) ? 0 : 1; const bAssigned = assignedSpoolIds.has(b.id) ? 0 : 1; if (aAssigned !== bAssigned) return aAssigned - bAssigned; return new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime(); }); }, [activeSpools, filterMode, searchQuery, assignedSpoolIds]); // Spoolman iframe mode const spoolmanEnabled = spoolmanSettings?.spoolman_enabled === 'true' && spoolmanSettings?.spoolman_url; if (spoolmanEnabled) { return (