This commit is contained in:
+151
-7
@@ -131,26 +131,170 @@ export async function renderPartTravelers(payload: {
|
||||
}): Promise<Uint8Array> {
|
||||
const doc = await PDFDocument.create();
|
||||
const fonts = await embedFonts(doc);
|
||||
await appendPartBundle(doc, fonts, payload);
|
||||
return doc.save();
|
||||
}
|
||||
|
||||
/** Entry point: one combined PDF for an entire project — every part bundle
|
||||
* concatenated (cover → drawings → op cards, repeated). Prefixed with a short
|
||||
* project-level summary page so the binder has a front sheet. Useful for
|
||||
* bulk reprints when a run changes.
|
||||
*/
|
||||
export async function renderProjectTravelers(payload: {
|
||||
project: {
|
||||
code: string;
|
||||
name: string;
|
||||
customerCode: string | null;
|
||||
dueDate: Date | null;
|
||||
};
|
||||
bundles: {
|
||||
cover: PartCoverData;
|
||||
cards: OperationCardData[];
|
||||
drawingPdfBytes?: Uint8Array | null;
|
||||
assemblyDrawingPdfBytes?: Uint8Array | null;
|
||||
}[];
|
||||
}): Promise<Uint8Array> {
|
||||
const doc = await PDFDocument.create();
|
||||
const fonts = await embedFonts(doc);
|
||||
|
||||
const summaryPage = doc.addPage([PAGE_WIDTH, PAGE_HEIGHT]);
|
||||
drawProjectSummary(summaryPage, fonts, payload.project, payload.bundles);
|
||||
|
||||
for (const bundle of payload.bundles) {
|
||||
await appendPartBundle(doc, fonts, bundle);
|
||||
}
|
||||
|
||||
return doc.save();
|
||||
}
|
||||
|
||||
/** Append one part's cover + drawings + op cards onto an existing doc. */
|
||||
async function appendPartBundle(
|
||||
doc: PDFDocument,
|
||||
fonts: Fonts,
|
||||
bundle: {
|
||||
cover: PartCoverData;
|
||||
cards: OperationCardData[];
|
||||
drawingPdfBytes?: Uint8Array | null;
|
||||
assemblyDrawingPdfBytes?: Uint8Array | null;
|
||||
},
|
||||
): Promise<void> {
|
||||
const coverPage = doc.addPage([PAGE_WIDTH, PAGE_HEIGHT]);
|
||||
await drawCoverSheet(doc, coverPage, fonts, payload.cover);
|
||||
await drawCoverSheet(doc, coverPage, fonts, bundle.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 (bundle.assemblyDrawingPdfBytes) {
|
||||
await appendPdfPages(doc, bundle.assemblyDrawingPdfBytes, "assembly drawing");
|
||||
}
|
||||
if (payload.drawingPdfBytes) {
|
||||
await appendPdfPages(doc, payload.drawingPdfBytes, "part drawing");
|
||||
if (bundle.drawingPdfBytes) {
|
||||
await appendPdfPages(doc, bundle.drawingPdfBytes, "part drawing");
|
||||
}
|
||||
|
||||
for (const card of payload.cards) {
|
||||
for (const card of bundle.cards) {
|
||||
const page = doc.addPage([PAGE_WIDTH, PAGE_HEIGHT]);
|
||||
await drawOperationCard(doc, page, fonts, card);
|
||||
}
|
||||
}
|
||||
|
||||
return doc.save();
|
||||
// Project-level front sheet. Deliberately minimal — the real detail lives on
|
||||
// each part's own cover sheet further in. Shows project header, a totals
|
||||
// line, and a table of contents listing every part being included.
|
||||
function drawProjectSummary(
|
||||
page: PDFPage,
|
||||
fonts: Fonts,
|
||||
project: { code: string; name: string; customerCode: string | null; dueDate: Date | null },
|
||||
bundles: { cover: PartCoverData; cards: OperationCardData[] }[],
|
||||
): void {
|
||||
const cursor: Cursor = { top: PAGE_HEIGHT - MARGIN };
|
||||
const contentWidth = PAGE_WIDTH - MARGIN * 2;
|
||||
|
||||
drawText(page, "PROJECT TRAVELERS", {
|
||||
x: MARGIN,
|
||||
y: cursor.top - 10,
|
||||
font: fonts.bold,
|
||||
size: 10,
|
||||
color: rgb(0.4, 0.4, 0.45),
|
||||
});
|
||||
cursor.top -= 28;
|
||||
|
||||
drawText(page, `${project.code} — ${project.name}`, {
|
||||
x: MARGIN,
|
||||
y: cursor.top,
|
||||
font: fonts.bold,
|
||||
size: 20,
|
||||
});
|
||||
cursor.top -= 20;
|
||||
|
||||
const metaBits = [
|
||||
project.customerCode ? `Customer ${project.customerCode}` : null,
|
||||
project.dueDate ? `Due ${project.dueDate.toISOString().slice(0, 10)}` : null,
|
||||
`${bundles.length} part${bundles.length === 1 ? "" : "s"}`,
|
||||
`${bundles.reduce((acc, b) => acc + b.cards.length, 0)} operations total`,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" · ");
|
||||
drawText(page, metaBits, {
|
||||
x: MARGIN,
|
||||
y: cursor.top,
|
||||
font: fonts.regular,
|
||||
size: 11,
|
||||
color: rgb(0.35, 0.4, 0.5),
|
||||
});
|
||||
cursor.top -= 20;
|
||||
|
||||
page.drawLine({
|
||||
start: { x: MARGIN, y: cursor.top },
|
||||
end: { x: PAGE_WIDTH - MARGIN, y: cursor.top },
|
||||
thickness: 0.75,
|
||||
color: rgb(0.8, 0.82, 0.88),
|
||||
});
|
||||
cursor.top -= 18;
|
||||
|
||||
drawText(page, "CONTENTS", {
|
||||
x: MARGIN,
|
||||
y: cursor.top,
|
||||
font: fonts.bold,
|
||||
size: 9,
|
||||
color: rgb(0.45, 0.5, 0.6),
|
||||
});
|
||||
cursor.top -= 14;
|
||||
|
||||
// Simple TOC. Long projects (50+ parts) will overflow the page; we cap the
|
||||
// visible rows and footnote the rest rather than spilling into a multi-page
|
||||
// TOC — the goal here is a summary, not a catalogue.
|
||||
const maxRows = 38;
|
||||
const rows = bundles.slice(0, maxRows);
|
||||
for (const b of rows) {
|
||||
const label = `${b.cover.assembly.code} · ${b.cover.part.code} — ${b.cover.part.name}`;
|
||||
const line = wrapLines(label, fonts.regular, 10, contentWidth - 60)[0] ?? label;
|
||||
drawText(page, line, {
|
||||
x: MARGIN,
|
||||
y: cursor.top,
|
||||
font: fonts.regular,
|
||||
size: 10,
|
||||
});
|
||||
const opsLabel = `${b.cards.length} op${b.cards.length === 1 ? "" : "s"}`;
|
||||
const opsLabelW = fonts.regular.widthOfTextAtSize(opsLabel, 10);
|
||||
drawText(page, opsLabel, {
|
||||
x: PAGE_WIDTH - MARGIN - opsLabelW,
|
||||
y: cursor.top,
|
||||
font: fonts.regular,
|
||||
size: 10,
|
||||
color: rgb(0.45, 0.5, 0.6),
|
||||
});
|
||||
cursor.top -= 13;
|
||||
}
|
||||
if (bundles.length > maxRows) {
|
||||
cursor.top -= 4;
|
||||
drawText(page, `… and ${bundles.length - maxRows} more part${bundles.length - maxRows === 1 ? "" : "s"}.`, {
|
||||
x: MARGIN,
|
||||
y: cursor.top,
|
||||
font: fonts.regular,
|
||||
size: 10,
|
||||
color: rgb(0.45, 0.5, 0.6),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Load an external PDF's pages and copy them into `doc`. Best-effort: if the
|
||||
|
||||
Reference in New Issue
Block a user