import { useState, useMemo } from 'react' import { useQuery } from '@tanstack/react-query' import { Search, Filter, ChevronLeft, ChevronRight, Trash2, Pencil, Plus, AlertTriangle } from 'lucide-react' import ColorSwatch from '../components/ColorSwatch' import { fetchFilaments, deleteFilament } from '../services/filamentService' import type { FilamentSpool, FilamentFilter } from '../types/filament' const PAGE_SIZE = 20 type SortField = 'name' | 'remaining_grams' | 'cost_usd' type SortDir = 'asc' | 'desc' export default function InventoryPage() { const [search, setSearch] = useState('') const [material, setMaterial] = useState('') const [finish, setFinish] = useState('') const [lowStockOnly, setLowStockOnly] = useState(false) const [sortBy, setSortBy] = useState('name') const [sortDir, setSortDir] = useState('asc') const [page, setPage] = useState(0) const [deleteId, setDeleteId] = useState(null) const filter: FilamentFilter = useMemo(() => ({ material: material || undefined, finish: finish || undefined, low_stock: lowStockOnly, sort_by: sortBy, sort_dir: sortDir, limit: PAGE_SIZE, offset: page * PAGE_SIZE, }), [material, finish, lowStockOnly, sortBy, sortDir, page]) const { data, isLoading, error, refetch } = useQuery({ queryKey: ['filaments', filter], queryFn: () => fetchFilaments(filter), }) const filaments = data?.data ?? [] const total = data?.total ?? 0 const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE)) // Client-side search filter (name/barcode) since backend may not support it yet. const filtered = useMemo(() => { if (!search.trim()) return filaments const q = search.toLowerCase() return filaments.filter( (f: FilamentSpool) => f.name.toLowerCase().includes(q) || (f.barcode && f.barcode.toLowerCase().includes(q)) ) }, [filaments, search]) const handleSort = (field: SortField) => { if (sortBy === field) { setSortDir(prev => (prev === 'asc' ? 'desc' : 'asc')) } else { setSortBy(field) setSortDir('asc') } setPage(0) } const handleDelete = async (id: number) => { await deleteFilament(id) setDeleteId(null) refetch() } const SortIndicator = ({ field }: { field: SortField }) => { if (sortBy !== field) return return {sortDir === 'asc' ? '↑' : '↓'} } return (
{/* Header */}

Filament Inventory

{total} spool(s) total

{/* Filters */}
{/* Search */}
{ setSearch(e.target.value); setPage(0) }} className="w-full rounded-lg bg-slate-800 border border-slate-700 pl-9 pr-3 py-2 text-sm text-slate-100 placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:border-emerald-500" />
{/* Material filter */} {/* Finish filter */} {/* Low stock toggle */}
{/* Loading / Error */} {isLoading && (
Loading spools…
)} {error && (
Failed to load inventory.
)} {/* Desktop Table */} {!isLoading && !error && ( <>
{filtered.length === 0 && ( )} {filtered.map((spool: FilamentSpool) => { const isLow = spool.remaining_grams <= spool.low_stock_threshold_grams return ( ) })}
handleSort('name')}> Name Material Finish Color handleSort('remaining_grams')}> Remaining handleSort('cost_usd')}> Cost Status Actions
No spools found.
{spool.name} {spool.material_base?.name ?? '—'} {spool.material_finish?.name ?? '—'}
{spool.color_hex}
{spool.remaining_grams.toLocaleString()} g {spool.cost_usd != null ? `$${spool.cost_usd.toFixed(2)}` : '—'} {isLow ? ( Low ) : ( OK )}
{/* Mobile Cards */}
{filtered.length === 0 && (
No spools found.
)} {filtered.map((spool: FilamentSpool) => { const isLow = spool.remaining_grams <= spool.low_stock_threshold_grams return (
{spool.name}
{spool.material_base?.name ?? '—'} · {spool.material_finish?.name ?? '—'}
{isLow ? ( Low ) : ( OK )}
{spool.color_hex}
Remaining: {spool.remaining_grams.toLocaleString()} g {spool.cost_usd != null ? `$${spool.cost_usd.toFixed(2)}` : '—'}
) })}
{/* Pagination */}
Showing {page * PAGE_SIZE + 1}–{Math.min((page + 1) * PAGE_SIZE, total)} of {total}
{page + 1} / {totalPages}
)} {/* Delete confirmation modal */} {deleteId !== null && (

Delete Spool?

This action cannot be undone.

)}
) }