"use client"; import { useMemo, useState } from "react"; import { useRouter } from "next/navigation"; import { Download, FileText } from "lucide-react"; import { buildJobCardPdf } from "@/lib/job-card-pdf"; import { downloadPdf } from "@/lib/qr-pdf"; import { inBounds, presetBounds, saDay, type LogPreset } from "@/lib/oil-log"; import { formatMinutes, JOB_TYPES, jobTypeLabel } from "@/lib/job-cards"; export type JobRow = { id: string; startedAt: string; endedAt: string; open: boolean; asset: string; assetName: string; mechanic: string; type: string; minutes: number; odometer: number | null; note: string; oil: string; oilLines: string[]; jcNo: number | null; site: string; }; const PRESETS: { id: LogPreset; label: string }[] = [ { id: "today", label: "Today" }, { id: "week", label: "This week" }, { id: "month", label: "This month" }, { id: "all", label: "Everything" }, { id: "range", label: "Date range" }, ]; const fmt = (n: number) => n.toLocaleString("en-ZA", { maximumFractionDigits: 1 }); const when = (s: string) => (s ? new Date(s.replace(" ", "T")).toLocaleString("en-ZA", { timeZone: "Africa/Johannesburg", day: "numeric", month: "short", hour: "2-digit", minute: "2-digit", hourCycle: "h23" }) : "—"); function pdfOf(r: JobRow) { return buildJobCardPdf({ jcNo: r.jcNo, plantNo: r.asset, plantName: r.assetName, site: r.site, mechanic: r.mechanic, machineHours: r.odometer != null ? String(r.odometer) : "", startedAt: r.startedAt, endedAt: r.endedAt, type: r.type, minutes: r.minutes, note: r.note, oil: r.oilLines, }); } export function JobCardsTable({ rows }: { rows: JobRow[] }) { const router = useRouter(); const [closing, setClosing] = useState(null); const [deleting, setDeleting] = useState(null); const [cType, setCType] = useState(""); const [cNote, setCNote] = useState(""); const [cOdo, setCOdo] = useState(""); const [cEnd, setCEnd] = useState(""); const [working, setWorking] = useState(false); const [dialogError, setDialogError] = useState(""); const [pdfBusy, setPdfBusy] = useState(""); async function download(r: JobRow | null) { setPdfBusy(r?.id ?? "blank"); try { const bytes = r ? await pdfOf(r) : await buildJobCardPdf({}); downloadPdf(bytes, r ? `job-card-${String(r.jcNo ?? 0).padStart(4, "0")}-${r.asset || "vehicle"}.pdf` : "job-card-blank.pdf"); } catch { alert("Could not build the PDF. Please try again."); } finally { setPdfBusy(""); } } function openClose(r: JobRow) { setClosing(r); setCType(""); setCNote(""); setCOdo(""); setCEnd(""); setDialogError(""); } async function submitClose(e: React.FormEvent) { e.preventDefault(); if (!closing || !cType) return; setWorking(true); setDialogError(""); try { const res = await fetch(`/api/job-cards/${closing.id}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ job_type: cType, note: cNote, odometer: cOdo, ended_at: cEnd ? new Date(cEnd).toISOString() : undefined }) }); const j = await res.json().catch(() => ({})); if (!res.ok || !j.ok) throw new Error(j.error || "Could not close the job"); setClosing(null); router.refresh(); } catch (err: any) { setDialogError(err.message); } finally { setWorking(false); } } async function confirmDelete() { if (!deleting) return; setWorking(true); setDialogError(""); try { const res = await fetch(`/api/job-cards/${deleting.id}`, { method: "DELETE" }); const j = await res.json().catch(() => ({})); if (!res.ok || !j.ok) throw new Error(j.error || "Could not delete the job"); setDeleting(null); router.refresh(); } catch (err: any) { setDialogError(err.message); } finally { setWorking(false); } } const [preset, setPreset] = useState("month"); const [from, setFrom] = useState(""); const [to, setTo] = useState(""); const [type, setType] = useState(""); const [asset, setAsset] = useState(""); const [mechanic, setMechanic] = useState(""); const assets = useMemo(() => [...new Set(rows.map((r) => r.asset))].filter(Boolean).sort(), [rows]); const mechanics = useMemo(() => [...new Set(rows.map((r) => r.mechanic))].filter(Boolean).sort(), [rows]); const shown = useMemo(() => { const bounds = presetBounds(preset, saDay(new Date().toISOString()), from, to); return rows.filter((r) => inBounds(saDay(r.startedAt), bounds) && (!type || r.type === type) && (!asset || r.asset === asset) && (!mechanic || r.mechanic === mechanic)); }, [rows, preset, from, to, type, asset, mechanic]); const done = shown.filter((r) => !r.open); const totalMinutes = done.reduce((s, r) => s + r.minutes, 0); const field = "min-h-[44px] rounded-lg border border-gray-200 dark:border-border bg-white dark:bg-surface px-3 text-sm"; return (
{PRESETS.map((p) => ( ))}
{preset === "range" && (
setFrom(e.target.value)} aria-label="From" /> to setTo(e.target.value)} aria-label="To" />
)}
{shown.length} job{shown.length !== 1 && "s"} {formatMinutes(totalMinutes)} on finished jobs {shown.some((r) => r.open) && {shown.filter((r) => r.open).length} in progress}
{shown.length === 0 ? (

{rows.length === 0 ? "No job cards yet. Mechanics start one by scanning a vehicle's QR in the Plant Clock app." : "No jobs match these filters."}

) : (
{shown.map((r) => ( ))}
Vehicle Mechanic Started Finished Time Work Odometer Oil used Note Actions
{r.asset || "—"}{r.assetName} {r.mechanic || "—"} {when(r.startedAt)} {r.open ? In progress : when(r.endedAt)} {r.open ? "—" : formatMinutes(r.minutes)} {jobTypeLabel(r.type) || "—"} {r.odometer != null ? fmt(r.odometer) : "—"} {r.oil || "—"} {r.note || "—"}
{r.open && }
)} {closing && (
!working && setClosing(null)}>
e.stopPropagation()} onSubmit={submitClose} role="dialog" aria-modal="true" aria-label="Close job" className="w-full max-w-md max-h-[90svh] overflow-y-auto rounded-2xl bg-white dark:bg-surface p-5 shadow-xl">

Close job on {closing.asset}

Started by {closing.mechanic || "—"} · {when(closing.startedAt)}. Use this when the mechanic did not scan the second time.

Kind of work

{JOB_TYPES.map((t) => ( ))}