Files
mrp-qrcode/app/api/v1/parts/[id]/operations/reorder/route.ts
T
2026-04-21 08:56:51 -05:00

88 lines
2.9 KiB
TypeScript

import { type NextRequest } from "next/server";
import { prisma } from "@/lib/prisma";
import { ok, errorResponse, requireRole, parseJson, ApiError } from "@/lib/api";
import { ReorderOperationsSchema } from "@/lib/schemas";
import { audit } from "@/lib/audit";
import { clientIp } from "@/lib/request";
/**
* Resequence the operations on a part. Body: { order: [opId, opId, ...] }
* The list must contain every operation currently on the part, in the
* desired top-to-bottom order. Uses a two-phase update to stay inside the
* (partId, sequence) unique constraint.
*/
export async function POST(req: NextRequest, ctx: { params: Promise<{ id: string }> }) {
try {
const actor = await requireRole("admin");
const { id: partId } = await ctx.params;
const { order } = await parseJson(req, ReorderOperationsSchema);
const existing = await prisma.operation.findMany({
where: { partId },
select: { id: true, sequence: true, status: true },
orderBy: { sequence: "asc" },
});
if (existing.length === 0) throw new ApiError(404, "not_found", "No operations on this part");
if (order.length !== existing.length) {
throw new ApiError(
400,
"incomplete_order",
"Order must list every operation on this part exactly once",
);
}
const ids = new Set(existing.map((o) => o.id));
const seen = new Set<string>();
for (const id of order) {
if (!ids.has(id)) throw new ApiError(400, "unknown_op", `Operation ${id} is not on this part`);
if (seen.has(id)) throw new ApiError(400, "duplicate_op", `Operation ${id} listed twice`);
seen.add(id);
}
// Refuse if any non-pending op would land at a different sequence number.
const byId = new Map(existing.map((o) => [o.id, o]));
for (let i = 0; i < order.length; i++) {
const target = i + 1;
const op = byId.get(order[i])!;
if (op.sequence !== target && op.status !== "pending") {
throw new ApiError(
409,
"op_not_pending",
`Cannot move operation ${op.id} — it is already ${op.status}`,
);
}
}
await prisma.$transaction(async (tx) => {
// Phase 1: park every row at a negative sequence to clear the unique slot.
for (let i = 0; i < order.length; i++) {
await tx.operation.update({
where: { id: order[i] },
data: { sequence: -(i + 1) },
});
}
// Phase 2: write the final sequence values.
for (let i = 0; i < order.length; i++) {
await tx.operation.update({
where: { id: order[i] },
data: { sequence: i + 1 },
});
}
});
await audit({
actorId: actor.id,
action: "reorder",
entity: "Part",
entityId: partId,
before: existing,
after: order.map((id, i) => ({ id, sequence: i + 1 })),
ipAddress: clientIp(req),
});
return ok({ ok: true });
} catch (err) {
return errorResponse(err);
}
}