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); } }