import { type NextRequest } from "next/server"; import { prisma } from "@/lib/prisma"; import { ok, errorResponse, requireRole, parseJson, ApiError } from "@/lib/api"; import { UpdateOperationSchema } from "@/lib/schemas"; import { audit } from "@/lib/audit"; import { clientIp } from "@/lib/request"; export async function GET(_req: NextRequest, ctx: { params: Promise<{ id: string }> }) { try { await requireRole("admin"); const { id } = await ctx.params; const operation = await prisma.operation.findUnique({ where: { id }, include: { machine: { select: { id: true, name: true } }, template: { select: { id: true, name: true } }, claimedBy: { select: { id: true, name: true } }, part: { select: { id: true, code: true, name: true, assembly: { select: { id: true, code: true, project: { select: { id: true, code: true, name: true } }, }, }, }, }, }, }); if (!operation) throw new ApiError(404, "not_found", "Operation not found"); return ok({ operation }); } catch (err) { return errorResponse(err); } } export async function PATCH(req: NextRequest, ctx: { params: Promise<{ id: string }> }) { try { const actor = await requireRole("admin"); const { id } = await ctx.params; const body = await parseJson(req, UpdateOperationSchema); const before = await prisma.operation.findUnique({ where: { id } }); if (!before) throw new ApiError(404, "not_found", "Operation not found"); // Refuse to mutate a claimed / in-progress op's sequence — it would scramble // the QR card the operator is currently holding. Content edits are allowed. if (body.sequence !== undefined && body.sequence !== before.sequence) { if (before.status !== "pending") { throw new ApiError( 409, "op_not_pending", "Cannot resequence an operation that is already in progress or completed", ); } const conflict = await prisma.operation.findUnique({ where: { partId_sequence: { partId: before.partId, sequence: body.sequence } }, select: { id: true }, }); if (conflict && conflict.id !== id) throw new ApiError(409, "sequence_taken", `Sequence ${body.sequence} already in use`); } const updated = await prisma.operation.update({ where: { id }, data: { ...(body.templateId !== undefined ? { templateId: body.templateId } : {}), ...(body.name !== undefined ? { name: body.name } : {}), ...(body.machineId !== undefined ? { machineId: body.machineId } : {}), ...(body.settings !== undefined ? { settings: body.settings } : {}), ...(body.materialNotes !== undefined ? { materialNotes: body.materialNotes } : {}), ...(body.instructions !== undefined ? { instructions: body.instructions } : {}), ...(body.qcRequired !== undefined ? { qcRequired: body.qcRequired } : {}), ...(body.plannedMinutes !== undefined ? { plannedMinutes: body.plannedMinutes } : {}), ...(body.plannedUnits !== undefined ? { plannedUnits: body.plannedUnits } : {}), ...(body.sequence !== undefined ? { sequence: body.sequence } : {}), ...(body.status !== undefined ? { status: body.status } : {}), }, }); await audit({ actorId: actor.id, action: "update", entity: "Operation", entityId: id, before, after: updated, ipAddress: clientIp(req), }); return ok({ operation: updated }); } catch (err) { return errorResponse(err); } } export async function DELETE(req: NextRequest, ctx: { params: Promise<{ id: string }> }) { try { const actor = await requireRole("admin"); const { id } = await ctx.params; const before = await prisma.operation.findUnique({ where: { id } }); if (!before) throw new ApiError(404, "not_found", "Operation not found"); if (before.status === "in_progress") { throw new ApiError( 409, "op_in_progress", "Release the claim before deleting an in-progress operation", ); } await prisma.operation.delete({ where: { id } }); await audit({ actorId: actor.id, action: "delete", entity: "Operation", entityId: id, before, ipAddress: clientIp(req), }); return ok({ ok: true }); } catch (err) { return errorResponse(err); } }