Files
mrp-qrcode/app/api/v1/operations/[id]/close/route.ts
T
jason c8c86c9ca4
Build and Push Docker Image / build (push) Successful in 43s
partial complete fixes
2026-04-22 07:15:56 -05:00

136 lines
5.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { type NextRequest } from "next/server";
import { prisma } from "@/lib/prisma";
import { ok, errorResponse, requireRole, parseJson, ApiError } from "@/lib/api";
import { CloseOperationSchema } from "@/lib/schemas";
import { audit } from "@/lib/audit";
import { clientIp } from "@/lib/request";
/**
* Complete an operation. Only the current claim holder may close, and if
* the operation is flagged qcRequired the payload must include an inline
* QC block (pass/fail + optional measurements). Close does four things
* atomically:
*
* 1. marks the operation completed + records completedAt,
* 2. closes the open TimeLog with unitsProcessed / note if provided,
* 3. writes a QCRecord if this op requires QC (or if the operator passed
* one in voluntarily),
* 4. audits the close for later reporting.
*/
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, CloseOperationSchema);
const existing = await prisma.operation.findUnique({
where: { id },
select: {
id: true,
status: true,
claimedByUserId: true,
qcRequired: true,
unitsCompleted: true,
part: {
select: {
qty: true,
assembly: { select: { qty: 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 complete this step");
}
if (existing.status !== "in_progress") {
throw new ApiError(409, "op_not_active", "Step is not active");
}
if (existing.qcRequired && !body.qc) {
throw new ApiError(400, "qc_required", "This step requires an inline QC check before completing");
}
// --- Partial vs fully-done detection --------------------------------
// The operator hits "Done" but may have only produced some of the
// assembly.qty × part.qty total units (e.g. end-of-shift handoff). In
// that case the step should end up `partial` so the NEXT operator can
// resume it — NOT `completed`, which locks it and requires admin to
// reset. We infer the operator's intent from the units they typed in:
//
// units == 0 (blank) → "trust me, it's done" → completed
// units >= remaining → math agrees → completed
// units < remaining → partial (clear claim, no completedAt)
//
// QC-required ops still demand a QC record even on partial close —
// that's about material checks, not about finishing the batch.
const units = body.unitsProcessed ?? 0;
const totalUnits = existing.part.assembly.qty * existing.part.qty;
const remaining = Math.max(0, totalUnits - existing.unitsCompleted);
const wouldFinish = units === 0 || units >= remaining;
const nextStatus: "completed" | "partial" = wouldFinish ? "completed" : "partial";
const now = new Date();
await prisma.$transaction(async (tx) => {
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,
},
});
}
if (body.qc) {
await tx.qCRecord.create({
data: {
operationId: id,
operatorId: actor.id,
kind: "inline",
passed: body.qc.passed,
measurements: body.qc.measurements ?? null,
notes: body.qc.notes ?? null,
},
});
}
// unitsCompleted is cumulative across pause/resume cycles; on close we
// add this session's batch so the total reflects everything the step
// actually produced. Partial close releases the claim so the next
// operator can resume; completed close sets completedAt for reporting.
await tx.operation.update({
where: { id },
data: {
status: nextStatus,
completedAt: nextStatus === "completed" ? now : null,
claimedByUserId: null,
claimedAt: null,
...(units > 0 ? { unitsCompleted: { increment: units } } : {}),
},
});
});
const op = await prisma.operation.findUnique({ where: { id } });
await audit({
actorId: actor.id,
action: nextStatus === "completed" ? "close_op" : "partial_close_op",
entity: "Operation",
entityId: id,
after: {
status: nextStatus,
unitsProcessed: body.unitsProcessed ?? null,
unitsRemaining: Math.max(0, remaining - units),
qcPassed: body.qc?.passed ?? null,
},
ipAddress: clientIp(req),
});
return ok({ operation: op, partial: nextStatus === "partial" });
} catch (err) {
return errorResponse(err);
}
}