"use client"; import { useState, type FormEvent } from "react"; import { useApiList } from "@/hooks/use-api-list"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Textarea } from "@/components/ui/textarea"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table"; // Shape returned by GET /api/maintenance-records — snake_case per this // repo's API-boundary convention (see src/lib/maintenance-records.ts's // toRecord()). An "open" record is one where end_date is null. interface MaintenanceRecordRecord { id: string; asset_item_id: string; type: "repair" | "warranty" | "preventive"; vendor_id: string | null; issue_description: string; resolution_notes: string | null; start_date: string; end_date: string | null; created_at: string; } // Shape returned by GET /api/assets — only the field used to populate the // assetItemId here (see src/app/vendors/page.tsx's VendorRecord for // the full shape). interface VendorRecord { id: string; name: string; } // The 4 AssetItemStatus enum values, reused verbatim as the targetStatus // ). const ASSET_ITEM_STATUS_VALUES = ["in_stock", "assigned", "repair", "retired"] as const; // Radix can't take an empty-string value, so "no vendor" is // represented by this sentinel in the form state and translated to "omit // the key" on submit — same pattern as licenses page's // NO_PURCHASE_ORDER_VALUE / employees page's NO_MANAGER_VALUE. const NO_VENDOR_VALUE = "none"; function todayDateString(): string { return new Date().toISOString().slice(0, 10); } export default function MaintenanceRecordsPage() { const { data: maintenanceRecords, loading, error, refetch, } = useApiList("/api/maintenance-records"); const { data: assetItems } = useApiList("/api/assets"); const { data: vendors } = useApiList("/api/vendors"); const [assetItemId, setAssetItemId] = useState(""); const [type, setType] = useState(""); const [vendorId, setVendorId] = useState(NO_VENDOR_VALUE); const [issueDescription, setIssueDescription] = useState(""); const [startDate, setStartDate] = useState(""); const [submitting, setSubmitting] = useState(false); const [submitError, setSubmitError] = useState(null); // Per-row close mini-form state, keyed by maintenance record id — kept as // separate maps (rather than one nested-object map) to keep each field's // setter a plain string setter, same shape as returnErrors/returningId on // assignments/page.tsx which this page mirrors. const [closeEndDate, setCloseEndDate] = useState>({}); const [closeResolutionNotes, setCloseResolutionNotes] = useState>({}); const [closeTargetStatus, setCloseTargetStatus] = useState>({}); const [closeErrors, setCloseErrors] = useState>({}); const [closingId, setClosingId] = useState(null); async function handleSubmit(event: FormEvent) { event.preventDefault(); setSubmitError(null); setSubmitting(true); try { const response = await fetch("/api/maintenance-records", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ assetItemId, type, issueDescription, startDate, // Omit the key entirely when left unselected, rather than sending // null — matches licenses page's purchaseOrderId pattern. ...(vendorId !== NO_VENDOR_VALUE ? { vendorId } : {}), }), }); let body: unknown; try { body = await response.json(); } catch { body = null; } if (!response.ok) { const message = body !== null && typeof body === "object" && "error" in body && typeof (body as { error: unknown }).error === "string" ? (body as { error: string }).error : "Request failed"; setSubmitError(message); return; } setAssetItemId(""); setType(""); setVendorId(NO_VENDOR_VALUE); setIssueDescription(""); setStartDate(""); refetch(); } catch { setSubmitError("Request failed"); } finally { setSubmitting(false); } } async function handleClose(id: string) { setClosingId(id); setCloseErrors((previous) => { const next = { ...previous }; delete next[id]; return next; }); const endDate = closeEndDate[id] ?? todayDateString(); const resolutionNotes = closeResolutionNotes[id] ?? ""; const targetStatus = closeTargetStatus[id] ?? ""; try { // PATCH /api/maintenance-records/[id]'s body is // `{endDate, resolutionNotes?, targetStatus}` — see src/lib/ // maintenance-records.ts's validateCloseMaintenanceRecordInput(). // Unlike Assignment's returnedAt, targetStatus is REQUIRED and never // defaulted server-side, so it's always sent here (an empty string // if unselected, which the server rejects with a clean 400 message // surfaced below, same as any other validation failure). const response = await fetch(`/api/maintenance-records/${id}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ endDate, targetStatus, ...(resolutionNotes.trim() !== "" ? { resolutionNotes } : {}), }), }); let body: unknown; try { body = await response.json(); } catch { body = null; } if (!response.ok) { const message = body !== null && typeof body === "object" && "error" in body && typeof (body as { error: unknown }).error === "string" ? (body as { error: string }).error : "Request failed"; setCloseErrors((previous) => ({ ...previous, [id]: message })); return; } setCloseEndDate((previous) => { const next = { ...previous }; delete next[id]; return next; }); setCloseResolutionNotes((previous) => { const next = { ...previous }; delete next[id]; return next; }); setCloseTargetStatus((previous) => { const next = { ...previous }; delete next[id]; return next; }); refetch(); } catch { setCloseErrors((previous) => ({ ...previous, [id]: "Request failed" })); } finally { setClosingId(null); } } return (

Maintenance Records

{loading ? (

Loading...

) : error ? (

{error}

) : ( ID Asset Item ID Type Vendor ID Issue Description Resolution Notes Start Date End Date Created At Actions {maintenanceRecords.map((record) => ( {record.id} {record.asset_item_id} {record.type} {record.vendor_id ?? "-"} {record.issue_description} {record.resolution_notes ?? "-"} {record.start_date} {record.end_date ?? "-"} {record.created_at} {record.end_date === null ? (
setCloseEndDate((previous) => ({ ...previous, [record.id]: event.target.value, })) } /> setCloseResolutionNotes((previous) => ({ ...previous, [record.id]: event.target.value, })) } /> {closeErrors[record.id] ? (

{closeErrors[record.id]}

) : null}
) : ( "-" )}
))}
)}

New Maintenance Record