+45
-5
@@ -20,7 +20,9 @@ const MARGIN = 48; // 2/3"
|
||||
|
||||
export interface OperationCardData {
|
||||
project: { code: string; name: string };
|
||||
assembly: { code: string; name: string };
|
||||
/** `qty` is the number of assemblies of this kind in the project. */
|
||||
assembly: { code: string; name: string; qty: number };
|
||||
/** `qty` is the per-assembly part count (so total parts = assembly.qty × part.qty). */
|
||||
part: { code: string; name: string; material: string | null; qty: number };
|
||||
operation: {
|
||||
id: string;
|
||||
@@ -59,7 +61,7 @@ export interface PurchaseOrderPdfData {
|
||||
|
||||
export interface PartCoverData {
|
||||
project: { code: string; name: string };
|
||||
assembly: { code: string; name: string };
|
||||
assembly: { code: string; name: string; qty: number };
|
||||
part: {
|
||||
code: string;
|
||||
name: string;
|
||||
@@ -98,10 +100,21 @@ export async function renderPurchaseOrder(data: PurchaseOrderPdfData): Promise<U
|
||||
return doc.save();
|
||||
}
|
||||
|
||||
/** Entry point: cover sheet + every operation card, all in one PDF. */
|
||||
/** Entry point: cover sheet + every operation card, all in one PDF.
|
||||
*
|
||||
* If `drawingPdfBytes` is provided (raw bytes of the part's PDF drawing),
|
||||
* those pages are inlined right after the cover sheet so the printed stack
|
||||
* is: cover → drawing(s) → op 1 → op 2 … Operators see the drawing on the
|
||||
* same sheet they're holding while running the part — no separate print.
|
||||
*
|
||||
* Assembly-level drawings can be appended too (`assemblyDrawingPdfBytes`),
|
||||
* rendered before the part drawing.
|
||||
*/
|
||||
export async function renderPartTravelers(payload: {
|
||||
cover: PartCoverData;
|
||||
cards: OperationCardData[];
|
||||
drawingPdfBytes?: Uint8Array | null;
|
||||
assemblyDrawingPdfBytes?: Uint8Array | null;
|
||||
}): Promise<Uint8Array> {
|
||||
const doc = await PDFDocument.create();
|
||||
const fonts = await embedFonts(doc);
|
||||
@@ -109,6 +122,16 @@ export async function renderPartTravelers(payload: {
|
||||
const coverPage = doc.addPage([PAGE_WIDTH, PAGE_HEIGHT]);
|
||||
await drawCoverSheet(doc, coverPage, fonts, payload.cover);
|
||||
|
||||
// Inline the assembly-level drawing first, then the part drawing. Both are
|
||||
// optional. We swallow per-PDF errors so a corrupt drawing doesn't block
|
||||
// the op cards from printing.
|
||||
if (payload.assemblyDrawingPdfBytes) {
|
||||
await appendPdfPages(doc, payload.assemblyDrawingPdfBytes, "assembly drawing");
|
||||
}
|
||||
if (payload.drawingPdfBytes) {
|
||||
await appendPdfPages(doc, payload.drawingPdfBytes, "part drawing");
|
||||
}
|
||||
|
||||
for (const card of payload.cards) {
|
||||
const page = doc.addPage([PAGE_WIDTH, PAGE_HEIGHT]);
|
||||
await drawOperationCard(doc, page, fonts, card);
|
||||
@@ -117,6 +140,19 @@ export async function renderPartTravelers(payload: {
|
||||
return doc.save();
|
||||
}
|
||||
|
||||
// Load an external PDF's pages and copy them into `doc`. Best-effort: if the
|
||||
// upstream PDF is unreadable we log to stderr (server-side) and skip; the
|
||||
// caller's traveler PDF is still produced.
|
||||
async function appendPdfPages(doc: PDFDocument, bytes: Uint8Array, label: string): Promise<void> {
|
||||
try {
|
||||
const src = await PDFDocument.load(bytes, { ignoreEncryption: true });
|
||||
const pages = await doc.copyPages(src, src.getPageIndices());
|
||||
for (const p of pages) doc.addPage(p);
|
||||
} catch (err) {
|
||||
console.warn(`[travelers.pdf] skipped ${label}: ${(err as Error).message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Layout helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -258,10 +294,11 @@ async function drawOperationCard(
|
||||
drawText(page, line, { x: MARGIN, y: cursor.top, font: fonts.bold, size: 22 });
|
||||
}
|
||||
cursor.top -= 14;
|
||||
const totalUnits = data.assembly.qty * data.part.qty;
|
||||
const partMeta = [
|
||||
`Part ${data.part.code}`,
|
||||
data.part.material ? `${data.part.material}` : null,
|
||||
`qty ${data.part.qty}`,
|
||||
`${data.part.qty}/assembly × ${data.assembly.qty} assy = ${totalUnits} to produce`,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" · ");
|
||||
@@ -471,9 +508,12 @@ async function drawCoverSheet(
|
||||
});
|
||||
cursor.top -= 18;
|
||||
|
||||
const totalUnits = data.assembly.qty * data.part.qty;
|
||||
const meta = [
|
||||
data.part.material ? `Material: ${data.part.material}` : null,
|
||||
`Quantity: ${data.part.qty}`,
|
||||
`Per-assembly qty: ${data.part.qty}`,
|
||||
`Assemblies: ${data.assembly.qty}`,
|
||||
`Total to produce: ${totalUnits}`,
|
||||
`Project: ${data.project.name}`,
|
||||
`Assembly: ${data.assembly.name}`,
|
||||
].filter(Boolean) as string[];
|
||||
|
||||
+8
-1
@@ -163,6 +163,9 @@ export const UpdateAssemblySchema = z
|
||||
name: NonEmpty.optional(),
|
||||
qty: z.coerce.number().int().positive().max(100000).optional(),
|
||||
notes: OptionalText,
|
||||
stepFileId: z.string().min(1).nullable().optional(),
|
||||
drawingFileId: z.string().min(1).nullable().optional(),
|
||||
cutFileId: z.string().min(1).nullable().optional(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
@@ -202,7 +205,11 @@ export const UpdatePartSchema = z
|
||||
|
||||
// ---- operations ---------------------------------------------------------
|
||||
|
||||
export const OperationStatuses = ["pending", "in_progress", "completed"] as const;
|
||||
// "partial" = an operation that was started, had units logged, and then paused.
|
||||
// Behaves like "pending" for claim purposes (any operator can resume it) but
|
||||
// visually distinct so admins can see work-in-flight that isn't actively
|
||||
// being run right now.
|
||||
export const OperationStatuses = ["pending", "in_progress", "partial", "completed"] as const;
|
||||
|
||||
export const CreateOperationSchema = z.object({
|
||||
templateId: z.string().min(1).nullable().optional(),
|
||||
|
||||
Reference in New Issue
Block a user