From 7f4a727669481fe0803e02006d6f041709de1afb Mon Sep 17 00:00:00 2001 From: toanhoang Date: Tue, 4 Aug 2026 05:32:21 -0400 Subject: [PATCH] feat: add MaintenanceRecord API routes POST/GET /api/maintenance-records and PATCH /api/maintenance-records/[id] (close action), mirroring the Assignment route pattern exactly. --- src/app/api/maintenance-records/[id]/route.ts | 60 +++++++++++++++++++ src/app/api/maintenance-records/route.ts | 58 ++++++++++++++++++ 2 files changed, 118 insertions(+) create mode 100644 src/app/api/maintenance-records/[id]/route.ts create mode 100644 src/app/api/maintenance-records/route.ts diff --git a/src/app/api/maintenance-records/[id]/route.ts b/src/app/api/maintenance-records/[id]/route.ts new file mode 100644 index 0000000..e1e6df6 --- /dev/null +++ b/src/app/api/maintenance-records/[id]/route.ts @@ -0,0 +1,60 @@ +import { NextResponse } from "next/server"; +import { z } from "zod"; +import { auth } from "@/auth"; +import { + closeMaintenanceRecord, + validateCloseMaintenanceRecordInput, +} from "@/lib/maintenance-records"; + +// Mirrors src/app/api/assignments/[id]/route.ts's pattern — PATCH is the +// only update action for MaintenanceRecord ("close"), no dispatch-on-body +// needed here since there is no second action shape (unlike Assignment's +// return-vs-transfer). Next.js 14 App Router route handler params are a +// plain synchronous object (not a Promise) — see the Assignment route's +// same comment for why. +export async function PATCH( + request: Request, + { params }: { params: { id: string } } +): Promise { + const session = await auth(); + if (!session) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + // Validate the [id] path segment's format up front — an invalid UUID + // passed straight to Prisma's `findUnique({ where: { id } })` (a `@db.Uuid` + // column) throws a raw PrismaClientKnownRequestError that + // closeMaintenanceRecord()'s try/catch doesn't specifically handle, which + // would otherwise leak as an uncaught 500 instead of a clean 400. + if (!z.string().uuid().safeParse(params.id).success) { + return NextResponse.json({ error: "id must be a valid UUID string" }, { status: 400 }); + } + + let body: unknown; + try { + body = await request.json(); + } catch { + return NextResponse.json({ error: "Request body must be valid JSON" }, { status: 400 }); + } + + if (typeof body !== "object" || body === null) { + return NextResponse.json({ error: "Request body must be a JSON object" }, { status: 400 }); + } + + const { endDate, resolutionNotes, targetStatus } = body as Record; + const validation = validateCloseMaintenanceRecordInput({ + endDate, + resolutionNotes, + targetStatus, + }); + + if (!validation.ok) { + return NextResponse.json({ error: validation.error }, { status: 400 }); + } + + const closed = await closeMaintenanceRecord(params.id, validation.value); + if (!closed.ok) { + return NextResponse.json({ error: closed.error }, { status: closed.status }); + } + return NextResponse.json(closed.value, { status: 200 }); +} diff --git a/src/app/api/maintenance-records/route.ts b/src/app/api/maintenance-records/route.ts new file mode 100644 index 0000000..3d8f58e --- /dev/null +++ b/src/app/api/maintenance-records/route.ts @@ -0,0 +1,58 @@ +import { NextResponse } from "next/server"; +import { auth } from "@/auth"; +import { + createMaintenanceRecord, + listMaintenanceRecords, + validateCreateMaintenanceRecordInput, +} from "@/lib/maintenance-records"; + +// Mirrors src/app/api/assignments/route.ts's pattern — middleware.ts +// excludes /api from route protection, so this checks the session itself. +// Flat permission model (CLAUDE.md): only "logged in or not", no role check. +export async function POST(request: Request): Promise { + const session = await auth(); + if (!session) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + let body: unknown; + try { + body = await request.json(); + } catch { + return NextResponse.json({ error: "Request body must be valid JSON" }, { status: 400 }); + } + + if (typeof body !== "object" || body === null) { + return NextResponse.json({ error: "Request body must be a JSON object" }, { status: 400 }); + } + + const { assetItemId, type, vendorId, issueDescription, startDate } = + body as Record; + const validation = validateCreateMaintenanceRecordInput({ + assetItemId, + type, + vendorId, + issueDescription, + startDate, + }); + + if (!validation.ok) { + return NextResponse.json({ error: validation.error }, { status: 400 }); + } + + const created = await createMaintenanceRecord(validation.value); + if (!created.ok) { + return NextResponse.json({ error: created.error }, { status: 400 }); + } + return NextResponse.json(created.value, { status: 201 }); +} + +export async function GET(): Promise { + const session = await auth(); + if (!session) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + const records = await listMaintenanceRecords(); + return NextResponse.json(records, { status: 200 }); +}