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.
79 lines
2.5 KiB
JavaScript
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}"`);
|
|
}
|