import { useState, useMemo } from 'react' import { useQuery } from '@tanstack/react-query' import { Search, Wifi, WifiOff, HelpCircle, Pencil, Plus, Trash2, AlertTriangle, ChevronLeft, ChevronRight, Server, Radio, ShieldCheck, } from 'lucide-react' import { fetchPrinters } from '../services/printerService' import type { Printer } from '../types/printer' const PAGE_SIZE = 20 type SortField = 'name' | 'printer_type' | 'manufacturer' | 'model' type SortDir = 'asc' | 'desc' type ConnStatus = 'online' | 'offline' | 'unknown' export default function PrintersPage() { const [search, setSearch] = useState('') const [statusFilter, setStatusFilter] = useState('') const [typeFilter, setTypeFilter] = useState('') const [sortBy, setSortBy] = useState('name') const [sortDir, setSortDir] = useState('asc') const [page, setPage] = useState(0) const [deleteId, setDeleteId] = useState(null) const { data: printers, isLoading, error, refetch, } = useQuery({ queryKey: ['printers'], queryFn: fetchPrinters, }) // Derive connection status from backend is_active for now // TODO: replace with real status endpoint / SSE when available const printersWithStatus = useMemo(() => { return (printers ?? []).map((p) => { const status: ConnStatus = p.is_active ? 'online' : 'offline' return { ...p, connection_status: status } }) }, [printers]) // Client-side filter + sort const filtered = useMemo(() => { let list = [...printersWithStatus] if (search.trim()) { const q = search.toLowerCase() list = list.filter( (p) => p.name.toLowerCase().includes(q) || (p.manufacturer?.toLowerCase().includes(q) ?? false) || (p.model?.toLowerCase().includes(q) ?? false) ) } if (statusFilter) { list = list.filter((p) => p.connection_status === statusFilter) } if (typeFilter) { list = list.filter((p) => p.printer_type?.name === typeFilter) } list.sort((a, b) => { const dir = sortDir === 'asc' ? 1 : -1 switch (sortBy) { case 'name': return a.name.localeCompare(b.name) * dir case 'printer_type': return (a.printer_type?.name ?? '').localeCompare(b.printer_type?.name ?? '') * dir case 'manufacturer': return (a.manufacturer ?? '').localeCompare(b.manufacturer ?? '') * dir case 'model': return (a.model ?? '').localeCompare(b.model ?? '') * dir default: return 0 } }) return list }, [printersWithStatus, search, statusFilter, typeFilter, sortBy, sortDir]) const total = filtered.length const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE)) const pageItems = filtered.slice(page * PAGE_SIZE, (page + 1) * PAGE_SIZE) const uniqueTypes = useMemo(() => { const s = new Set() ;(printers ?? []).forEach((p) => { if (p.printer_type?.name) s.add(p.printer_type.name) }) return Array.from(s).sort() }, [printers]) 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 deletePrinter(id) setDeleteId(null) refetch() } const SortIndicator = ({ field }: { field: SortField }) => { if (sortBy !== field) return return {sortDir === 'asc' ? '↑' : '↓'} } const StatusBadge = ({ status }: { status: ConnStatus }) => { if (status === 'online') { return ( Online ) } if (status === 'offline') { return ( Offline ) } return ( Unknown ) } return (
{/* Header */}

Printers

{total} printer(s)

{/* 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" />
{/* Status filter */} {/* Type filter */}
{/* Loading / Error */} {isLoading &&
Loading printers…
} {error && (
Failed to load printers.
)} {/* Desktop Table */} {!isLoading && !error && ( <>
{pageItems.length === 0 && ( )} {pageItems.map((p: Printer & { connection_status: ConnStatus }) => ( ))}
handleSort('name')} > Name handleSort('printer_type')} > Type handleSort('manufacturer')} > Manufacturer handleSort('model')} > Model Connection Moonraker MQTT Actions
No printers found.
{p.name} {p.printer_type?.name ?? '—'} {p.manufacturer ?? '—'} {p.model ?? '—'} {p.moonraker_url ? ( {p.moonraker_url} ) : ( )} {p.mqtt_broker_host ? ( {p.mqtt_topic_prefix ? `${p.mqtt_topic_prefix} @ ` : ''} {p.mqtt_broker_host} {p.mqtt_tls_enabled && ( )} ) : ( )}
{/* Mobile Cards */}
{pageItems.length === 0 && (
No printers found.
)} {pageItems.map((p: Printer & { connection_status: ConnStatus }) => (
{p.name}
{p.printer_type?.name ?? '—'} · {p.manufacturer ?? '—'} {p.model ?? ''}
{/* Moonraker */} {p.moonraker_url && ( )} {/* MQTT */} {p.mqtt_broker_host && (
{p.mqtt_topic_prefix ? `${p.mqtt_topic_prefix} @ ` : ''} {p.mqtt_broker_host} {p.mqtt_tls_enabled && ( )}
)} {/* Actions */}
))}
{/* Pagination */}
Showing {page * PAGE_SIZE + 1}–{Math.min((page + 1) * PAGE_SIZE, total)} of {total}
{page + 1} / {totalPages}
)} {/* Delete confirmation modal */} {deleteId !== null && (

Delete Printer?

This action cannot be undone.

)}
) }