JEISA-WEBAPP/scripts/preview-quote-pdf.mjs
Dave a30270e77f fix(quotes): wrap item descriptions on the quote PDF
Item descriptions were cut off with an ellipsis. Long names like
"PRO132T-C24 - CARBON BRUSH EACH, PRO152T" lost their tail, which on a
quotation reads as a mistake.

Descriptions now wrap onto as many lines as they need and the row grows
to fit, so the full name is always printed. SKU, quantity, price and
total stay on the first line of the row.

While wrapping, the quote PDF gained the page handling the purchase
order PDF already had: a row that no longer fits moves to a new page
with the column headers repeated, and the totals and notes blocks
break to a new page rather than running off the bottom. Before this
the quote PDF had no page breaks at all, so a long quote simply ran
off the page.

Addresses also keep their line breaks now instead of being flattened
into one line.

The two quote PDF builders - "Download PDF" and the copy stored in
PocketBase - were near-identical copies of ~275 lines. They now share
one builder (quotePdf.ts), so a layout fix can no longer land in one
and miss the other.
2026-10-21 08:40:39 +02:00

79 lines
2.5 KiB
JavaScript

#!/usr/bin/env node
/**
* Renders a quote PDF against live data so the layout can be checked by
* eye - especially the wrapped item descriptions.
*
* Usage:
* node --experimental-strip-types scripts/preview-quote-pdf.mjs <quote_number> [out.pdf]
*
* Example:
* node --experimental-strip-types scripts/preview-quote-pdf.mjs QT-20260914-001 /tmp/quote.pdf
*/
import PocketBase from "pocketbase";
import fs from "node:fs/promises";
import { buildQuotePdf } from "../src/components/quotes/quotePdf.ts";
const PB_URL = process.env.POCKETBASE_URL || "https://pb.jeistk.apphub.co.za";
const PB_EMAIL = process.env.POCKETBASE_ADMIN_EMAIL || "david@wfsales.co.za";
const PB_PASSWORD = process.env.POCKETBASE_ADMIN_PASSWORD || "1234567890";
const QUOTE_NUMBER = process.argv[2] || "QT-20260914-001";
const OUT = process.argv[3] || `/tmp/quote-${QUOTE_NUMBER}.pdf`;
const pb = new PocketBase(PB_URL);
pb.autoCancellation(false);
await pb.admins.authWithPassword(PB_EMAIL, PB_PASSWORD);
const rec = await pb.collection("quotes").getFirstListItem(
`quote_number = "${QUOTE_NUMBER}"`,
{ expand: "customerName" },
);
const cust = rec.expand?.customerName || {};
const quantities = Array.isArray(rec.quantities) ? rec.quantities : [];
const unitPrices = Array.isArray(rec.unitPrices) ? rec.unitPrices : [];
const itemIds = Array.isArray(rec.items) ? rec.items : [];
let items = [];
if (itemIds.length) {
const filter = itemIds.map((id) => `id = "${id}"`).join(" || ");
const stock = await pb.collection("stock_items").getList(1, 200, { filter });
const byId = new Map(stock.items.map((s) => [s.id, s]));
items = itemIds.map((id, i) => ({
stockItemId: id,
itemName: byId.get(id)?.name || "(item)",
sku: byId.get(id)?.sku || "",
sellingPrice: unitPrices[i] || 0,
}));
}
const quote = {
id: rec.id,
quote_number: rec.quote_number,
customerName: cust.comapny_name || "",
customerRef: rec.customerRef || "",
quoteDate: rec.quoteDate,
validUntil: rec.validUntil,
status: rec.status,
quantities,
unitPrices,
items,
customer: {
name: cust.comapny_name || "",
address: cust.address || "",
email: cust.email || "",
phone: cust.phone || "",
vatNumber: cust.vatNumber || "",
},
notes: rec.notes || "",
};
const doc = buildQuotePdf(quote);
await fs.writeFile(OUT, Buffer.from(doc.output("arraybuffer")));
console.log(`Wrote ${OUT}`);
console.log(` customer: ${quote.customer.name}`);
console.log(` items : ${items.length}`);
for (const it of items) {
console.log(` - "${it.itemName}"`);
}