stage 5-6
Build and Push Docker Image / build (push) Successful in 1m11s

This commit is contained in:
jason
2026-04-21 13:14:27 -05:00
parent fc5bce4868
commit 5847a175af
26 changed files with 3031 additions and 29 deletions
@@ -0,0 +1,105 @@
import { type NextRequest, NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";
import { errorResponse, requireRole, ApiError } from "@/lib/api";
import {
renderPartTravelers,
type OperationCardData,
type PartCoverData,
} from "@/lib/pdf";
/**
* Admin-only. Return a single PDF containing:
* Page 1 - cover sheet (part header, file manifest, operation summary)
* Page 2..N - one traveler card per operation, sequence order.
*
* Matches the physical print workflow: staple the stack, the cover goes on
* top for the project binder and the cards go out to the floor.
*/
export async function GET(_req: NextRequest, ctx: { params: Promise<{ id: string }> }) {
try {
await requireRole("admin");
const { id } = await ctx.params;
const part = await prisma.part.findUnique({
where: { id },
include: {
assembly: {
include: { project: { select: { code: true, name: true } } },
},
stepFile: { select: { originalName: true, sizeBytes: true, sha256: true } },
drawingFile: { select: { originalName: true, sizeBytes: true, sha256: true } },
cutFile: { select: { originalName: true, sizeBytes: true, sha256: true } },
operations: {
orderBy: { sequence: "asc" },
include: { machine: { select: { name: true, kind: true } } },
},
},
});
if (!part) throw new ApiError(404, "not_found", "Part not found");
if (part.operations.length === 0) {
throw new ApiError(400, "no_operations", "This part has no operations to print");
}
const project = part.assembly.project;
const assembly = { code: part.assembly.code, name: part.assembly.name };
const partHeader = {
code: part.code,
name: part.name,
material: part.material,
qty: part.qty,
};
const cover: PartCoverData = {
project,
assembly,
part: { ...partHeader, notes: part.notes },
files: [
{ label: "STEP / 3D", file: part.stepFile },
{ label: "Drawing PDF", file: part.drawingFile },
{ label: "Cut file", file: part.cutFile },
],
operations: part.operations.map((op) => ({
sequence: op.sequence,
name: op.name,
machineName: op.machine?.name ?? null,
qcRequired: op.qcRequired,
qrToken: op.qrToken,
})),
};
const cards: OperationCardData[] = part.operations.map((op) => ({
project,
assembly,
part: partHeader,
operation: {
id: op.id,
sequence: op.sequence,
name: op.name,
qrToken: op.qrToken,
machineName: op.machine?.name ?? null,
machineKind: op.machine?.kind ?? null,
settings: op.settings,
materialNotes: op.materialNotes,
instructions: op.instructions,
qcRequired: op.qcRequired,
plannedMinutes: op.plannedMinutes,
plannedUnits: op.plannedUnits,
},
}));
const pdf = await renderPartTravelers({ cover, cards });
const safeName = `${part.code}-travelers.pdf`.replace(/[^A-Za-z0-9._-]/g, "_");
return new NextResponse(pdf as unknown as BodyInit, {
status: 200,
headers: {
"Content-Type": "application/pdf",
"Content-Disposition": `inline; filename="${safeName}"`,
"Cache-Control": "private, no-store",
},
});
} catch (err) {
return errorResponse(err);
}
}