import { type NextRequest } from "next/server"; import { prisma } from "@/lib/prisma"; import { ok, errorResponse, requireRole, parseJson, ApiError } from "@/lib/api"; import { CreateOperationSchema } from "@/lib/schemas"; import { audit } from "@/lib/audit"; import { clientIp } from "@/lib/request"; import { generateQrToken } from "@/lib/qr"; export async function GET(_req: NextRequest, ctx: { params: Promise<{ id: string }> }) { try { await requireRole("admin"); const { id } = await ctx.params; const operations = await prisma.operation.findMany({ where: { partId: id }, orderBy: { sequence: "asc" }, include: { machine: { select: { id: true, name: true } }, template: { select: { id: true, name: true } }, }, }); return ok({ operations }); } catch (err) { return errorResponse(err); } } export async function POST(req: NextRequest, ctx: { params: Promise<{ id: string }> }) { try { const actor = await requireRole("admin"); const { id } = await ctx.params; const body = await parseJson(req, CreateOperationSchema); const part = await prisma.part.findUnique({ where: { id } }); if (!part) throw new ApiError(404, "not_found", "Part not found"); // If a template is referenced, fetch it so we can inherit unspecified defaults. let template: { id: string; machineId: string | null; defaultSettings: string | null; defaultInstructions: string | null; qcRequired: boolean; active: boolean; } | null = null; if (body.templateId) { template = await prisma.operationTemplate.findUnique({ where: { id: body.templateId }, select: { id: true, machineId: true, defaultSettings: true, defaultInstructions: true, qcRequired: true, active: true, }, }); if (!template || !template.active) throw new ApiError(400, "invalid_template", "Operation template not available"); } // Resolve sequence: explicit value wins, else append to end. let sequence = body.sequence; if (!sequence) { const max = await prisma.operation.aggregate({ where: { partId: id }, _max: { sequence: true }, }); sequence = (max._max.sequence ?? 0) + 1; } else { const conflict = await prisma.operation.findUnique({ where: { partId_sequence: { partId: id, sequence } }, select: { id: true }, }); if (conflict) throw new ApiError(409, "sequence_taken", `Sequence ${sequence} already in use on this part`); } // Derive effective values, falling back to the template where the caller left fields blank. const effectiveMachineId = body.machineId !== undefined ? body.machineId : (template?.machineId ?? null); const effectiveSettings = body.settings !== undefined && body.settings !== null ? body.settings : (template?.defaultSettings ?? null); const effectiveInstructions = body.instructions !== undefined && body.instructions !== null ? body.instructions : (template?.defaultInstructions ?? null); const effectiveQcRequired = body.qcRequired || template?.qcRequired || false; // Generate a unique qrToken. Collisions at 192 bits are vanishingly rare; retry anyway. let qrToken = generateQrToken(); for (let attempt = 0; attempt < 5; attempt++) { const existing = await prisma.operation.findUnique({ where: { qrToken }, select: { id: true }, }); if (!existing) break; qrToken = generateQrToken(); if (attempt === 4) throw new ApiError(500, "qr_collision", "Unable to allocate QR token"); } const created = await prisma.operation.create({ data: { partId: id, sequence, templateId: template?.id ?? null, name: body.name, kind: body.kind ?? "work", machineId: effectiveMachineId, settings: effectiveSettings, materialNotes: body.materialNotes ?? null, instructions: effectiveInstructions, // Dedicated inspection ops are always QC-on-close — force the flag on // at create time so downstream code doesn't have to special-case kind. qcRequired: (body.kind ?? "work") === "qc" ? true : effectiveQcRequired, plannedMinutes: body.plannedMinutes ?? null, plannedUnits: body.plannedUnits ?? null, qrToken, }, }); await audit({ actorId: actor.id, action: "create", entity: "Operation", entityId: created.id, after: created, ipAddress: clientIp(req), }); return ok({ operation: created }, { status: 201 }); } catch (err) { return errorResponse(err); } }