"use client" import { useCallback, useEffect, useMemo, useState } from "react" import Link from "next/link" import { useParams } from "next/navigation" import { toast } from "sonner" import { AlertTriangle, Car, ChevronLeft, ChevronRight, CircleCheck, FileSignature, Gauge, Image as ImageIcon, Loader2, Share2, StickyNote, User, } from "lucide-react" import { Button } from "@/shared/components/ui/button" import { Badge } from "@/shared/components/ui/badge" import { Card, CardAction, CardContent, CardDescription, CardHeader, CardTitle, } from "@/shared/components/ui/card" import { Progress } from "@/shared/components/ui/progress" import { Separator } from "@/shared/components/ui/separator" import { Tabs, TabsList, TabsTrigger } from "@/shared/components/ui/tabs" import { useAuthApi } from "@/shared/useApi" import { CheckpointFillDialog, isCheckpointComplete, type Checkpoint, type Severity, } from "@/modules/inspections/checkpoint-fill-dialog" import { SignaturePad } from "@/modules/inspections/signature-pad" import { InspectionShareDialog } from "@/modules/inspections/inspection-share-dialog" type Inspection = { id: number title: string order_number?: string | null status?: string | null odometer?: number | null template?: { id: number; name: string } | null customer?: { first_name?: string; last_name?: string } | null vehicle?: { make?: string; model?: string; year?: string | number; license_plate?: string } | null check_points: Checkpoint[] technician_signature_url?: string | null customer_signature_url?: string | null } const SEVERITY_DOT: Record = { good: "bg-emerald-500 shadow-emerald-500/30", attention: "bg-amber-500 shadow-amber-500/30", critical: "bg-rose-500 shadow-rose-500/30", na: "bg-slate-400 shadow-slate-400/30", not_inspected: "bg-muted shadow-none", } const SEVERITY_LABEL: Record = { good: "Good", attention: "Attention", critical: "Critical", na: "N/A", not_inspected: "Not inspected", } const SEVERITY_BADGE: Record = { good: "default", attention: "secondary", critical: "destructive", na: "outline", not_inspected: "outline", } function groupBySection(cps: Checkpoint[]) { const map = new Map() for (const cp of cps) { const key = cp.section_name || "Other" if (!map.has(key)) map.set(key, []) map.get(key)!.push(cp) } return Array.from(map.entries()).map(([name, items]) => ({ name, items })) } export default function InspectionCheckpointsPage() { const params = useParams<{ id: string }>() const id = params.id const api = useAuthApi() const [data, setData] = useState(null) const [initialLoad, setInitialLoad] = useState(true) const [refetching, setRefetching] = useState(false) const [activeIndex, setActiveIndex] = useState(null) const [shareOpen, setShareOpen] = useState(false) const [mode, setMode] = useState<"all" | "wizard">("all") const [wizardSectionIdx, setWizardSectionIdx] = useState(0) const [signing, setSigning] = useState<{ who: "technician" | "customer" } | null>(null) const load = useCallback(async () => { const isFirst = !data if (isFirst) setInitialLoad(true) else setRefetching(true) try { const res = await api.inspections.showOne(id) setData(res.data as Inspection) } catch (e: any) { toast.error(e?.payload?.message ?? "Failed to load inspection") } finally { setInitialLoad(false) setRefetching(false) } // eslint-disable-next-line react-hooks/exhaustive-deps }, [id]) const patchCheckpoint = useCallback((checkpointId: number, patch: Partial) => { setData((prev) => { if (!prev) return prev return { ...prev, check_points: prev.check_points.map((c) => c.id === checkpointId ? { ...c, ...patch } : c ), } }) }, []) useEffect(() => { load() }, [load]) const sections = useMemo(() => data ? groupBySection(data.check_points ?? []) : [], [data]) const flatCheckpoints = useMemo(() => sections.flatMap((s) => s.items), [sections]) const totals = useMemo(() => { const t = { good: 0, attention: 0, critical: 0, na: 0, not_inspected: 0 } for (const cp of data?.check_points ?? []) { const s = (cp.severity as Severity) ?? "not_inspected" t[s]++ } return t }, [data]) const setSeverity = async (cp: Checkpoint, severity: Severity) => { patchCheckpoint(cp.id, { severity }) try { await api.inspections.updateCheckpoint(String(cp.id), { severity } as any) } catch (e: any) { toast.error(e?.payload?.message ?? "Failed to save") load() } } const handleSign = async (who: "technician" | "customer", dataUrl: string) => { setSigning({ who }) try { await api.inspections.sign(id, who, dataUrl) toast.success(who === "technician" ? "Technician signature saved" : "Customer signature saved") load() } catch (e: any) { toast.error(e?.payload?.message ?? "Failed to save signature") } finally { setSigning(null) } } if (initialLoad) { return (
Loading inspection…
) } if (!data) return
Inspection not found
const vehicleLine = data.vehicle ? [data.vehicle.year, data.vehicle.make, data.vehicle.model].filter(Boolean).join(" ") : "—" const customerName = data.customer ? `${data.customer.first_name ?? ""} ${data.customer.last_name ?? ""}`.trim() : "—" const totalCount = data.check_points.length const inspectedCount = totalCount - totals.not_inspected const progress = totalCount > 0 ? Math.round((inspectedCount / totalCount) * 100) : 0 const wizardSection = sections[wizardSectionIdx] return (
{/* Header */}
{refetching && }
Inspection {data.title}
{customerName} {vehicleLine} {data.vehicle?.license_plate && ( {data.vehicle.license_plate} )} {data.odometer != null && ( {Number(data.odometer).toLocaleString()} km )}
{/* Progress */}
Progress {inspectedCount} / {totalCount} ({progress}%)
{/* Severity tally */}
{(["good", "attention", "critical", "na", "not_inspected"] as Severity[]).map((s) => ( {totals[s]} {SEVERITY_LABEL[s]} ))}
{/* Legend */}
Tap a dot to mark: {(["good", "attention", "critical", "na"] as Severity[]).map((s) => ( {SEVERITY_LABEL[s]} ))}
{/* Mode tabs */} {totalCount > 0 && ( setMode(v as any)} className="w-full"> All sections Wizard )} {/* Sections */} {totalCount === 0 ? (

This inspection has no checkpoints yet. It was probably created from an empty template.

) : mode === "all" ? ( sections.map((section) => (
{section.name} {section.items.filter(isCheckpointComplete).length} / {section.items.length} done
    {section.items.map((cp) => ( setActiveIndex(flatCheckpoints.findIndex((c) => c.id === cp.id))} /> ))}
)) ) : ( wizardSection && (() => { const pendingItems = wizardSection.items.filter((cp) => !isCheckpointComplete(cp)) const sectionDone = pendingItems.length === 0 return (
{wizardSection.name} Section {wizardSectionIdx + 1} / {sections.length}
    {wizardSection.items.map((cp) => ( setActiveIndex(flatCheckpoints.findIndex((c) => c.id === cp.id))} /> ))}
{!sectionDone && (
{pendingItems.length} pending.{' '} Each checkpoint needs a finding and its required recording before you can move on.
)}
) })() )} {/* Signatures */} {totalCount > 0 && ( Sign-off handleSign("technician", d)} /> handleSign("customer", d)} /> )} { if (!o) setActiveIndex(null) }} checkpoints={flatCheckpoints} activeIndex={activeIndex} onIndexChange={setActiveIndex} onSaved={load} onPatch={patchCheckpoint} />
) } function CheckpointRow({ cp, onSeverity, onOpen, }: { cp: Checkpoint onSeverity: (cp: Checkpoint, s: Severity) => void onOpen: () => void }) { const current = (cp.severity as Severity) ?? "not_inspected" const attachments = cp.attachments ?? cp.media ?? [] const photoCount = attachments.filter((m) => m.media_type === "photo").length const complete = isCheckpointComplete(cp) return (
  • {(["good", "attention", "critical", "na"] as Severity[]).map((s) => { const active = current === s return (
  • ) }