"use client" import { use, useState, useRef, useCallback } from "react" import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query" import { useAuthApi } from "@/shared/useApi" import { DataTable, ColumnHeader } from "@/shared/data-view/table-view" import DashboardPage from "@/base/components/layout/dashboard/dashboard-page" import { Badge } from "@/shared/components/ui/badge" import { Button } from "@/shared/components/ui/button" import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from "@/shared/components/ui/dropdown-menu" import { Dialog, DialogContent, DialogHeader, DialogTitle, } from "@/shared/components/ui/dialog" import { Input } from "@/shared/components/ui/input" import { Label } from "@/shared/components/ui/label" import { Textarea } from "@/shared/components/ui/textarea" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/shared/components/ui/select" import { toast } from "sonner" import { Plus, Ellipsis, Pencil, Trash2, CheckCircle2, AlertTriangle, XCircle, MinusCircle, CircleDot, Paperclip, FileUp, FileText, FileImage, File, X, } from "lucide-react" import { INSPECTION_ROUTES } from "@garage/api" // ── Types ── type CheckpointItem = { id: number inspection_id?: number name?: string description?: string record_type?: string condition_rate?: number file?: string status?: string created_at?: string updated_at?: string } // ── Constants ── const CHECKPOINT_STATUSES = [ { value: "passed", label: "Passed", icon: CheckCircle2, color: "bg-green-100 text-green-800" }, { value: "need_attention", label: "Need Attention", icon: AlertTriangle, color: "bg-yellow-100 text-yellow-800" }, { value: "failed", label: "Failed", icon: XCircle, color: "bg-red-100 text-red-800" }, { value: "not_applicable", label: "Not Applicable", icon: MinusCircle, color: "bg-gray-100 text-gray-800" }, { value: "not_inspected", label: "Not Inspected", icon: CircleDot, color: "bg-blue-100 text-blue-800" }, ] as const const RECORD_TYPES = [ { value: "record_conditions", label: "Record Conditions" }, { value: "record_audio", label: "Record Audio" }, { value: "record_video", label: "Record Video" }, { value: "capture_photo", label: "Capture Photo" }, ] as const function formatStatus(status?: string) { if (!status) return "Not Inspected" return status.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()) } function getStatusConfig(status?: string) { return CHECKPOINT_STATUSES.find((s) => s.value === status) || CHECKPOINT_STATUSES[4] } // ── Checkpoint Form Dialog ── function CheckpointFormDialog({ open, onOpenChange, inspectionId, checkpoint, onSuccess, }: { open: boolean onOpenChange: (open: boolean) => void inspectionId: string checkpoint?: CheckpointItem | null onSuccess: () => void }) { const api = useAuthApi() const [name, setName] = useState(checkpoint?.name ?? "") const [description, setDescription] = useState(checkpoint?.description ?? "") const [recordType, setRecordType] = useState(checkpoint?.record_type ?? "record_conditions") const [isPending, setIsPending] = useState(false) const isEditing = !!checkpoint const handleSubmit = async (e: React.FormEvent) => { e.preventDefault() if (!name.trim()) { toast.error("Name is required") return } setIsPending(true) try { const payload = { inspection_id: Number(inspectionId), name: name.trim(), description: description.trim() || undefined, record_type: recordType, } if (isEditing) { const promise = api.inspections.updateCheckpoint(String(checkpoint.id), payload) toast.promise(promise, { loading: "Updating checkpoint...", success: "Checkpoint updated", error: "Failed to update checkpoint", }) await promise } else { const promise = api.inspections.createCheckpoint(payload) toast.promise(promise, { loading: "Creating checkpoint...", success: "Checkpoint created", error: "Failed to create checkpoint", }) await promise } onSuccess() onOpenChange(false) } finally { setIsPending(false) } } return ( {isEditing ? "Edit Checkpoint" : "Add Checkpoint"} Name * setName(e.target.value)} required /> Description setDescription(e.target.value)} /> Record Type {RECORD_TYPES.map((rt) => ( {rt.label} ))} {isEditing ? : } {isPending ? (isEditing ? "Updating..." : "Creating...") : (isEditing ? "Update Checkpoint" : "Create Checkpoint")} ) } // ── Attachments Dialog ── function getFileIcon(url: string) { const ext = url.split(".").pop()?.toLowerCase() if (["png", "jpg", "jpeg", "gif", "webp", "svg"].includes(ext ?? "")) return FileImage if (ext === "pdf") return FileText return File } function getFileName(url: string) { try { return decodeURIComponent(url.split("/").pop() ?? "Attachment") } catch { return url.split("/").pop() ?? "Attachment" } } function isImageUrl(url: string) { const ext = url.split(".").pop()?.toLowerCase() return ["png", "jpg", "jpeg", "gif", "webp", "svg"].includes(ext ?? "") } function CheckpointAttachmentsDialog({ open, onOpenChange, checkpoint, onSuccess, }: { open: boolean onOpenChange: (open: boolean) => void checkpoint: CheckpointItem | null onSuccess: () => void }) { const api = useAuthApi() const fileInputRef = useRef(null) const [isUploading, setIsUploading] = useState(false) const handleUpload = useCallback(async (file: globalThis.File) => { if (!checkpoint) return setIsUploading(true) try { const promise = api.inspections.uploadCheckpointMedia( String(checkpoint.id), { file }, ) toast.promise(promise, { loading: "Uploading attachment...", success: "Attachment uploaded", error: "Failed to upload attachment", }) await promise onSuccess() } finally { setIsUploading(false) if (fileInputRef.current) fileInputRef.current.value = "" } }, [api, checkpoint, onSuccess]) const handleDelete = useCallback(async () => { if (!checkpoint) return const promise = api.inspections.deleteCheckpointMedia(String(checkpoint.id)) toast.promise(promise, { loading: "Removing attachment...", success: "Attachment removed", error: "Failed to remove attachment", }) await promise onSuccess() }, [api, checkpoint, onSuccess]) const hasFile = !!checkpoint?.file const FileIcon = checkpoint?.file ? getFileIcon(checkpoint.file) : File return ( Attachments — {checkpoint?.name} {/* Current attachment */} {hasFile ? ( {isImageUrl(checkpoint.file!) ? ( ) : ( )} {getFileName(checkpoint.file!)} Current attachment ) : ( No attachments yet )} {/* Upload area */} { const file = e.target.files?.[0] if (file) handleUpload(file) }} /> fileInputRef.current?.click()} > {isUploading ? "Uploading..." : hasFile ? "Replace Attachment" : "Add Attachment"} ) } // ── Main Page ── export default function InspectionCheckpointsPage({ params }: { params: Promise<{ id: string }> }) { const { id: inspectionId } = use(params) const api = useAuthApi() const queryClient = useQueryClient() const [formOpen, setFormOpen] = useState(false) const [editingCheckpoint, setEditingCheckpoint] = useState(null) const [attachmentsCheckpoint, setAttachmentsCheckpoint] = useState(null) const queryKey = [INSPECTION_ROUTES.CHECKPOINTS, inspectionId] const { data, isLoading } = useQuery({ queryKey, queryFn: () => api.inspections.listCheckpoints({ inspection_id: inspectionId } as never), }) const invalidate = () => { queryClient.invalidateQueries({ queryKey }) } // ── Status change mutation ── const changeStatusMutation = useMutation({ mutationFn: ({ checkpointId, status }: { checkpointId: number; status: string }) => api.inspections.changeCheckpointStatus({ inspection_id: Number(inspectionId), inspection_check_point_id: String(checkpointId), name: "", // required by API type but server uses checkpoint id from context record_type: "record_conditions", status, } as never), onSuccess: () => invalidate(), }) // ── Delete checkpoint mutation ── const deleteMutation = useMutation({ mutationFn: (checkpointId: string) => api.inspections.destroyCheckpoint(checkpointId), onSuccess: () => { toast.success("Checkpoint deleted") invalidate() }, onError: () => toast.error("Failed to delete checkpoint"), }) const handleEdit = (checkpoint: CheckpointItem) => { setEditingCheckpoint(checkpoint) setFormOpen(true) } const handleAdd = () => { setEditingCheckpoint(null) setFormOpen(true) } const handleStatusChange = (checkpointId: number, status: string) => { const promise = changeStatusMutation.mutateAsync({ checkpointId, status, }) toast.promise(promise, { loading: "Updating status...", success: `Status changed to ${formatStatus(status)}`, error: "Failed to update status", }) } const checkpoints = (data as any)?.data ?? [] const meta = (data as any)?.meta const pagination = { page: meta?.current_page ?? 1, pageSize: meta?.per_page ?? 15, pageCount: meta?.last_page ?? 1, total: meta?.total ?? 0, } const columns = [ { accessorKey: "name", header: ({ column }: any) => , cell: ({ row }: any) => { const item = row.original as CheckpointItem return ( {item.name} {item.description && ( {item.description} )} ) }, }, { accessorKey: "record_type", header: ({ column }: any) => , cell: ({ row }: any) => { const item = row.original as CheckpointItem const rt = RECORD_TYPES.find((r) => r.value === item.record_type) return rt?.label ?? item.record_type ?? "—" }, }, { accessorKey: "status", header: ({ column }: any) => , cell: ({ row }: any) => { const item = row.original as CheckpointItem const config = getStatusConfig(item.status) return ( {formatStatus(item.status)} ) }, }, { accessorKey: "condition_rate", header: ({ column }: any) => , cell: ({ row }: any) => { const item = row.original as CheckpointItem return item.condition_rate != null ? `${item.condition_rate}/10` : "—" }, }, { id: "attachments", header: () => Attachments, cell: ({ row }: any) => { const item = row.original as CheckpointItem return ( setAttachmentsCheckpoint(item)} > {item.file ? ( 1 ) : ( 0 )} ) }, enableSorting: false, }, { id: "actions", cell: ({ row }: any) => { const item = row.original as CheckpointItem return ( {CHECKPOINT_STATUSES.map((s) => ( handleStatusChange(item.id, s.value)} disabled={item.status === s.value} > {s.label} ))} handleEdit(item)}> Edit deleteMutation.mutate(String(item.id))} > Delete ) }, }, ] return ( Checkpoints Add Checkpoint {}} isLoading={isLoading} /> { setFormOpen(open) if (!open) setEditingCheckpoint(null) }} inspectionId={inspectionId} checkpoint={editingCheckpoint} onSuccess={invalidate} /> { if (!open) setAttachmentsCheckpoint(null) }} checkpoint={attachmentsCheckpoint} onSuccess={invalidate} /> ) }