import { type NextRequest } from "next/server"; import { prisma } from "@/lib/prisma"; import { ok, errorResponse, requireRole, parseJson, ApiError } from "@/lib/api"; import { ReleaseOperationSchema } from "@/lib/schemas"; import { audit } from "@/lib/audit"; import { clientIp } from "@/lib/request"; /** * Pause an in-progress operation: drop the claim, close the open TimeLog, * and send the step back to pending so any operator can pick it up next. * Only the current claim holder may release (admins get their own escape * hatch via the PATCH endpoint if we ever need one). */ export async function POST(req: NextRequest, ctx: { params: Promise<{ id: string }> }) { try { const actor = await requireRole("operator"); const { id } = await ctx.params; const body = await parseJson(req, ReleaseOperationSchema); const existing = await prisma.operation.findUnique({ where: { id }, select: { id: true, status: true, claimedByUserId: true }, }); if (!existing) throw new ApiError(404, "not_found", "Operation not found"); if (existing.claimedByUserId !== actor.id) { throw new ApiError(409, "not_claim_holder", "Only the current operator can pause this step"); } if (existing.status !== "in_progress") { throw new ApiError(409, "op_not_active", "Step is not active"); } // If the operator logged any units during this session we flip status to // `partial` (instead of `pending`) so the scan card can say "Resume this // step" and the counter survives across pauses. const units = body.unitsProcessed ?? 0; const nextStatus: "pending" | "partial" = units > 0 ? "partial" : "pending"; const now = new Date(); await prisma.$transaction(async (tx) => { // Close the most recent open TimeLog for (op, operator). We accept that // if two rows are open for the same pair something has gone wrong // elsewhere; close the newest and let the audit log preserve history. const openLog = await tx.timeLog.findFirst({ where: { operationId: id, operatorId: actor.id, endedAt: null }, orderBy: { startedAt: "desc" }, }); if (openLog) { await tx.timeLog.update({ where: { id: openLog.id }, data: { endedAt: now, unitsProcessed: body.unitsProcessed ?? null, note: body.note ?? null, }, }); } await tx.operation.update({ where: { id }, data: { status: nextStatus, claimedByUserId: null, claimedAt: null, ...(units > 0 ? { unitsCompleted: { increment: units } } : {}), }, }); }); const op = await prisma.operation.findUnique({ where: { id } }); await audit({ actorId: actor.id, action: "release_op", entity: "Operation", entityId: id, after: { status: nextStatus, unitsProcessed: body.unitsProcessed ?? null }, ipAddress: clientIp(req), }); return ok({ operation: op }); } catch (err) { return errorResponse(err); } }