@@ -39,6 +39,9 @@ export async function PATCH(req: NextRequest, ctx: { params: Promise<{ id: strin
|
||||
...(body.name !== undefined ? { name: body.name } : {}),
|
||||
...(body.qty !== undefined ? { qty: body.qty } : {}),
|
||||
...(body.notes !== undefined ? { notes: body.notes } : {}),
|
||||
...(body.stepFileId !== undefined ? { stepFileId: body.stepFileId } : {}),
|
||||
...(body.drawingFileId !== undefined ? { drawingFileId: body.drawingFileId } : {}),
|
||||
...(body.cutFileId !== undefined ? { cutFileId: body.cutFileId } : {}),
|
||||
},
|
||||
});
|
||||
await audit({
|
||||
|
||||
@@ -28,6 +28,7 @@ export async function GET(_req: NextRequest, ctx: { params: Promise<{ id: string
|
||||
select: {
|
||||
code: true,
|
||||
name: true,
|
||||
qty: true,
|
||||
project: { select: { code: true, name: true } },
|
||||
},
|
||||
},
|
||||
@@ -39,7 +40,11 @@ export async function GET(_req: NextRequest, ctx: { params: Promise<{ id: string
|
||||
|
||||
const data: OperationCardData = {
|
||||
project: op.part.assembly.project,
|
||||
assembly: { code: op.part.assembly.code, name: op.part.assembly.name },
|
||||
assembly: {
|
||||
code: op.part.assembly.code,
|
||||
name: op.part.assembly.name,
|
||||
qty: op.part.assembly.qty,
|
||||
},
|
||||
part: {
|
||||
code: op.part.code,
|
||||
name: op.part.name,
|
||||
|
||||
@@ -37,8 +37,11 @@ export async function POST(req: NextRequest, ctx: { params: Promise<{ id: string
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
// `partial` behaves exactly like `pending` for claim purposes — it just
|
||||
// means a previous operator paused with some units done. Either is fair
|
||||
// game to resume.
|
||||
const updateResult = await prisma.operation.updateMany({
|
||||
where: { id, claimedByUserId: null, status: "pending" },
|
||||
where: { id, claimedByUserId: null, status: { in: ["pending", "partial"] } },
|
||||
data: {
|
||||
status: "in_progress",
|
||||
claimedByUserId: actor.id,
|
||||
|
||||
@@ -66,6 +66,10 @@ export async function POST(req: NextRequest, ctx: { params: Promise<{ id: string
|
||||
},
|
||||
});
|
||||
}
|
||||
// unitsCompleted is cumulative across pause/resume cycles; on close we
|
||||
// add this session's batch so the total reflects everything the step
|
||||
// actually produced.
|
||||
const units = body.unitsProcessed ?? 0;
|
||||
await tx.operation.update({
|
||||
where: { id },
|
||||
data: {
|
||||
@@ -73,6 +77,7 @@ export async function POST(req: NextRequest, ctx: { params: Promise<{ id: string
|
||||
completedAt: now,
|
||||
claimedByUserId: null,
|
||||
claimedAt: null,
|
||||
...(units > 0 ? { unitsCompleted: { increment: units } } : {}),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -29,6 +29,12 @@ export async function POST(req: NextRequest, ctx: { params: Promise<{ id: string
|
||||
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
|
||||
@@ -50,7 +56,12 @@ export async function POST(req: NextRequest, ctx: { params: Promise<{ id: string
|
||||
}
|
||||
await tx.operation.update({
|
||||
where: { id },
|
||||
data: { status: "pending", claimedByUserId: null, claimedAt: null },
|
||||
data: {
|
||||
status: nextStatus,
|
||||
claimedByUserId: null,
|
||||
claimedAt: null,
|
||||
...(units > 0 ? { unitsCompleted: { increment: units } } : {}),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -60,7 +71,7 @@ export async function POST(req: NextRequest, ctx: { params: Promise<{ id: string
|
||||
action: "release_op",
|
||||
entity: "Operation",
|
||||
entityId: id,
|
||||
after: { status: "pending", unitsProcessed: body.unitsProcessed ?? null },
|
||||
after: { status: nextStatus, unitsProcessed: body.unitsProcessed ?? null },
|
||||
ipAddress: clientIp(req),
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { type NextRequest, NextResponse } from "next/server";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { errorResponse, requireRole, ApiError } from "@/lib/api";
|
||||
import { readFileBytes } from "@/lib/files";
|
||||
import {
|
||||
renderPartTravelers,
|
||||
type OperationCardData,
|
||||
@@ -24,10 +25,15 @@ export async function GET(_req: NextRequest, ctx: { params: Promise<{ id: string
|
||||
where: { id },
|
||||
include: {
|
||||
assembly: {
|
||||
include: { project: { select: { code: true, name: true } } },
|
||||
include: {
|
||||
project: { select: { code: true, name: true } },
|
||||
drawingFile: { select: { path: true, originalName: true } },
|
||||
},
|
||||
},
|
||||
stepFile: { select: { originalName: true, sizeBytes: true, sha256: true } },
|
||||
drawingFile: { select: { originalName: true, sizeBytes: true, sha256: true } },
|
||||
drawingFile: {
|
||||
select: { originalName: true, sizeBytes: true, sha256: true, path: true },
|
||||
},
|
||||
cutFile: { select: { originalName: true, sizeBytes: true, sha256: true } },
|
||||
operations: {
|
||||
orderBy: { sequence: "asc" },
|
||||
@@ -42,7 +48,11 @@ export async function GET(_req: NextRequest, ctx: { params: Promise<{ id: string
|
||||
}
|
||||
|
||||
const project = part.assembly.project;
|
||||
const assembly = { code: part.assembly.code, name: part.assembly.name };
|
||||
const assembly = {
|
||||
code: part.assembly.code,
|
||||
name: part.assembly.name,
|
||||
qty: part.assembly.qty,
|
||||
};
|
||||
const partHeader = {
|
||||
code: part.code,
|
||||
name: part.name,
|
||||
@@ -50,6 +60,14 @@ export async function GET(_req: NextRequest, ctx: { params: Promise<{ id: string
|
||||
qty: part.qty,
|
||||
};
|
||||
|
||||
// Pull drawing PDFs off disk so pdf-lib can inline them behind the cover /
|
||||
// op cards. Missing / unreadable files are logged and skipped — the
|
||||
// traveler still prints without them rather than 500ing.
|
||||
const drawingPdfBytes = await tryReadPdf(part.drawingFile?.path ?? null);
|
||||
const assemblyDrawingPdfBytes = await tryReadPdf(
|
||||
part.assembly.drawingFile?.path ?? null,
|
||||
);
|
||||
|
||||
const cover: PartCoverData = {
|
||||
project,
|
||||
assembly,
|
||||
@@ -88,7 +106,12 @@ export async function GET(_req: NextRequest, ctx: { params: Promise<{ id: string
|
||||
},
|
||||
}));
|
||||
|
||||
const pdf = await renderPartTravelers({ cover, cards });
|
||||
const pdf = await renderPartTravelers({
|
||||
cover,
|
||||
cards,
|
||||
drawingPdfBytes,
|
||||
assemblyDrawingPdfBytes,
|
||||
});
|
||||
const safeName = `${part.code}-travelers.pdf`.replace(/[^A-Za-z0-9._-]/g, "_");
|
||||
|
||||
return new NextResponse(pdf as unknown as BodyInit, {
|
||||
@@ -103,3 +126,14 @@ export async function GET(_req: NextRequest, ctx: { params: Promise<{ id: string
|
||||
return errorResponse(err);
|
||||
}
|
||||
}
|
||||
|
||||
async function tryReadPdf(path: string | null): Promise<Uint8Array | null> {
|
||||
if (!path) return null;
|
||||
try {
|
||||
const buf = await readFileBytes(path);
|
||||
return new Uint8Array(buf);
|
||||
} catch (err) {
|
||||
console.warn("[travelers.pdf] could not read drawing file", { path, err });
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user