QoL changes and additions
Build and Push Docker Image / build (push) Successful in 45s

This commit is contained in:
jason
2026-04-22 13:16:42 -05:00
parent a165428f14
commit 04ae88ca0d
14 changed files with 1424 additions and 29 deletions
+151 -7
View File
@@ -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