import { useState, useMemo } from 'react' import { useQuery } from '@tanstack/react-query' import { Search, Filter, ChevronLeft, ChevronRight, Loader2 } from 'lucide-react' import { fetchPrintJobs } from '../services/printJobService' import type { PrintJob, PrintJobFilter, PrintJobStatus } from '../types/printJob' const PAGE_SIZE = 20 type SortField = 'name' | 'started_at' | 'duration_seconds' | 'filament_grams_used' | 'cost_usd' type SortDir = 'asc' | 'desc' // ── Status helpers ──────────────────────────────────────────────────────────── const STATUS_CONFIG: Record = { pending: { label: 'Pending', badge: 'bg-slate-700 text-slate-300 border-slate-600', icon: '⏳' }, printing: { label: 'Printing', badge: 'bg-blue-900/40 text-blue-300 border-blue-700', icon: '🖨️' }, completed: { label: 'Completed', badge: 'bg-emerald-900/30 text-emerald-300 border-emerald-700', icon: '✅' }, failed: { label: 'Failed', badge: 'bg-red-900/40 text-red-300 border-red-700', icon: '❌' }, } function StatusBadge({ status }: { status: PrintJobStatus }) { const cfg = STATUS_CONFIG[status] ?? STATUS_CONFIG.pending return ( {cfg.icon} {cfg.label} ) } // ── Duration formatter ──────────────────────────────────────────────────────── function formatDuration(totalSeconds?: number): string { if (totalSeconds == null || totalSeconds <= 0) return '—' const h = Math.floor(totalSeconds / 3600) const m = Math.floor((totalSeconds % 3600) / 60) const s = totalSeconds % 60 if (h > 0) return `${h}h ${m}m` if (m > 0) return `${m}m ${s}s` return `${s}s` } function formatDateTime(iso: string): string { const d = new Date(iso) return d.toLocaleString(undefined, { month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit', }) } // ── Component ───────────────────────────────────────────────────────────────── export default function PrintJobsPage() { const [search, setSearch] = useState('') const [statusFilter, setStatusFilter] = useState('') const [printerFilter, setPrinterFilter] = useState('') const [sortBy, setSortBy] = useState('started_at') const [sortDir, setSortDir] = useState('desc') const [page, setPage] = useState(0) const filter: PrintJobFilter = useMemo(() => ({ status: statusFilter || undefined, printer: printerFilter || undefined, sort_by: sortBy, sort_dir: sortDir, limit: PAGE_SIZE, offset: page * PAGE_SIZE, }), [statusFilter, printerFilter, sortBy, sortDir, page]) const { data, isLoading, error, refetch } = useQuery({ queryKey: ['printJobs', filter], queryFn: () => fetchPrintJobs(filter), }) const total = data?.total ?? 0 const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE)) // Client-side search (name + printer name) const filtered = useMemo(() => { const jobList = data?.data ?? [] if (!search.trim()) return jobList const q = search.toLowerCase() return jobList.filter( (j: PrintJob) => j.name.toLowerCase().includes(q) || j.printer_name.toLowerCase().includes(q) ) }, [data?.data, search]) const handleSort = (field: SortField) => { if (sortBy === field) { setSortDir(prev => (prev === 'asc' ? 'desc' : 'asc')) } else { setSortBy(field) setSortDir('asc') } setPage(0) } const SortIndicator = ({ field }: { field: SortField }) => { if (sortBy !== field) return return {sortDir === 'asc' ? '↑' : '↓'} } return (
{/* Header */}

Print Jobs

{total} job(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" />
{/* Status filter */} {/* Printer filter */} { setPrinterFilter(e.target.value); setPage(0) }} className="rounded-lg bg-slate-800 border border-slate-700 px-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 min-w-[160px]" /> {/* Filter indicator */} {(statusFilter || printerFilter) && ( {[statusFilter && `Status: ${statusFilter}`, printerFilter && `Printer: ${printerFilter}`].filter(Boolean).join(' · ')} )}
{/* Loading / Error */} {isLoading && (
Loading print jobs…
)} {error && (
Failed to load print jobs.
)} {/* Desktop Table */} {!isLoading && !error && ( <>
{filtered.length === 0 && ( )} {filtered.map((job: PrintJob) => ( ))}
handleSort('name')}> Job Printer Status handleSort('started_at')}> Started handleSort('duration_seconds')}> Duration handleSort('filament_grams_used')}> Filament handleSort('cost_usd')}> Cost
No print jobs found.
{job.name} {job.printer_name} {formatDateTime(job.started_at)} {formatDuration(job.duration_seconds)} {job.filament_grams_used != null ? `${job.filament_grams_used.toLocaleString()} g` : '—'} {job.cost_usd != null ? `$${job.cost_usd.toFixed(2)}` : '—'}
{/* Mobile Cards */}
{filtered.length === 0 && (
No print jobs found.
)} {filtered.map((job: PrintJob) => (
{job.name}
{job.printer_name}
Started
{formatDateTime(job.started_at)}
Duration
{formatDuration(job.duration_seconds)}
Filament
{job.filament_grams_used != null ? `${job.filament_grams_used.toLocaleString()} g` : '—'}
Cost
{job.cost_usd != null ? `$${job.cost_usd.toFixed(2)}` : '—'}
))}
{/* Pagination */}
Showing {total === 0 ? 0 : page * PAGE_SIZE + 1}–{Math.min((page + 1) * PAGE_SIZE, total)} of {total}
{page + 1} / {totalPages}
)}
) }