182 lines
6.7 KiB
JavaScript
182 lines
6.7 KiB
JavaScript
|
|
#!/usr/bin/env node
|
||
|
|
/**
|
||
|
|
* Generates a real PDF for a given quote and saves it locally.
|
||
|
|
* Used to verify the PDF rendering works against live data.
|
||
|
|
*
|
||
|
|
* Usage: node scripts/test-quote-pdf.mjs <quote_number>
|
||
|
|
*/
|
||
|
|
import PocketBase from "pocketbase";
|
||
|
|
import { jsPDF } from "jspdf";
|
||
|
|
import fs from "node:fs/promises";
|
||
|
|
import path from "node:path";
|
||
|
|
|
||
|
|
const PB_URL = "https://pb.jeistk.apphub.co.za";
|
||
|
|
const PB_EMAIL = "david@wfsales.co.za";
|
||
|
|
const PB_PASSWORD = "1234567890";
|
||
|
|
const QUOTE_NUMBER = process.argv[2] || "QT-20260914-001";
|
||
|
|
|
||
|
|
const pb = new PocketBase(PB_URL);
|
||
|
|
pb.autoCancellation(false);
|
||
|
|
await pb.admins.authWithPassword(PB_EMAIL, PB_PASSWORD);
|
||
|
|
|
||
|
|
// Mirror the Quotes.tsx hydration logic
|
||
|
|
const record = await pb.collection("quotes").getFirstListItem(
|
||
|
|
`quote_number = "${QUOTE_NUMBER}"`,
|
||
|
|
{ expand: "customerName" }
|
||
|
|
);
|
||
|
|
|
||
|
|
const itemIds = Array.isArray(record.items) ? record.items : [];
|
||
|
|
const items = itemIds.map((id, i) => ({
|
||
|
|
stockItemId: id,
|
||
|
|
itemName: "",
|
||
|
|
quantity: record.quantities?.[i] ?? 0,
|
||
|
|
sku: "",
|
||
|
|
sellingPrice: record.unitPrices?.[i] ?? 0,
|
||
|
|
}));
|
||
|
|
|
||
|
|
if (itemIds.length > 0) {
|
||
|
|
const filter = itemIds.map((id) => `id = "${id}"`).join(" || ");
|
||
|
|
const stockRecs = await pb.collection("stock_items").getList(1, 200, { filter });
|
||
|
|
const byId = new Map(stockRecs.items.map((r) => [r.id, r]));
|
||
|
|
for (const it of items) {
|
||
|
|
const s = byId.get(it.stockItemId);
|
||
|
|
if (s) { it.itemName = s.name; it.sku = s.sku; }
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const customer = record.expand?.customerName;
|
||
|
|
const quote = {
|
||
|
|
id: record.id,
|
||
|
|
quote_number: record.quote_number,
|
||
|
|
customerName: customer?.comapny_name ?? "",
|
||
|
|
quoteDate: record.quoteDate,
|
||
|
|
validUntil: record.validUntil,
|
||
|
|
status: record.status,
|
||
|
|
quantities: record.quantities ?? [],
|
||
|
|
unitPrices: record.unitPrices ?? [],
|
||
|
|
items,
|
||
|
|
customer: customer ? { name: customer.comapny_name, address: "", email: "", phone: "" } : null,
|
||
|
|
notes: record.notes ?? "",
|
||
|
|
attachments: [],
|
||
|
|
};
|
||
|
|
|
||
|
|
console.log("Quote for PDF:", JSON.stringify({ n: quote.quote_number, items: quote.items.length, qty: quote.quantities }, null, 2));
|
||
|
|
|
||
|
|
// ---- Mirror the PDF generation from QuoteDataTable.tsx ----
|
||
|
|
const doc = new jsPDF();
|
||
|
|
const brandBlack = "#000000";
|
||
|
|
const accentYellow = "#ffc905";
|
||
|
|
const darkText = "#1a1a1a";
|
||
|
|
const lightGray = "#f5f5f5";
|
||
|
|
const mediumGray = "#666666";
|
||
|
|
|
||
|
|
doc.setFillColor(brandBlack);
|
||
|
|
doc.rect(0, 0, 210, 45, "F");
|
||
|
|
doc.setFillColor(accentYellow);
|
||
|
|
doc.rect(0, 45, 210, 2, "F");
|
||
|
|
doc.setFont("helvetica", "bold"); doc.setFontSize(20); doc.setTextColor(255, 255, 255);
|
||
|
|
doc.text("JEI SA", 14, 20);
|
||
|
|
doc.setFont("helvetica", "normal"); doc.setFontSize(9); doc.setTextColor(220, 220, 220);
|
||
|
|
doc.text("47 Station St, Oberholzer, Carletonville, 2499", 14, 26);
|
||
|
|
doc.text("Tel: 018 786 2897", 14, 31);
|
||
|
|
doc.setFont("helvetica", "bold"); doc.setFontSize(24); doc.setTextColor(255, 255, 255);
|
||
|
|
doc.text("QUOTATION", 120, 25);
|
||
|
|
|
||
|
|
doc.setFillColor(250, 250, 250);
|
||
|
|
doc.roundedRect(125, 55, 71, 35, 2, 2, "FD");
|
||
|
|
doc.setFont("helvetica", "bold"); doc.setFontSize(9); doc.setTextColor(mediumGray);
|
||
|
|
doc.text("Quote No:", 128, 61);
|
||
|
|
doc.text("Date:", 128, 69);
|
||
|
|
doc.text("Valid Until:", 128, 77);
|
||
|
|
doc.setFont("helvetica", "normal"); doc.setTextColor(darkText);
|
||
|
|
doc.text(quote.quote_number || quote.id, 193, 61, { align: "right" });
|
||
|
|
doc.text(new Date(quote.quoteDate).toLocaleDateString(), 193, 69, { align: "right" });
|
||
|
|
doc.text(quote.validUntil ? new Date(quote.validUntil).toLocaleDateString() : "N/A", 193, 77, { align: "right" });
|
||
|
|
|
||
|
|
let y = 105;
|
||
|
|
doc.setFont("helvetica", "bold"); doc.setFontSize(11); doc.setTextColor(brandBlack);
|
||
|
|
doc.text("CUSTOMER INFORMATION", 14, y);
|
||
|
|
doc.setDrawColor(accentYellow); doc.setLineWidth(1.5);
|
||
|
|
doc.line(14, y + 2, 75, y + 2);
|
||
|
|
y += 10;
|
||
|
|
doc.setFillColor(lightGray);
|
||
|
|
doc.roundedRect(14, y, 182, 32, 2, 2, "F");
|
||
|
|
y += 6;
|
||
|
|
doc.setFont("helvetica", "bold"); doc.setFontSize(10); doc.setTextColor(darkText);
|
||
|
|
doc.text(quote.customer?.name || quote.customerName || "N/A", 18, y);
|
||
|
|
|
||
|
|
y += 14;
|
||
|
|
doc.setFont("helvetica", "bold"); doc.setFontSize(11); doc.setTextColor(brandBlack);
|
||
|
|
doc.text("QUOTE ITEMS", 14, y);
|
||
|
|
doc.setDrawColor(accentYellow);
|
||
|
|
doc.line(14, y + 2, 55, y + 2);
|
||
|
|
y += 8;
|
||
|
|
|
||
|
|
doc.setFillColor(brandBlack);
|
||
|
|
doc.roundedRect(14, y, 182, 10, 1, 1, "F");
|
||
|
|
doc.setFont("helvetica", "bold"); doc.setFontSize(9); doc.setTextColor(255, 255, 255);
|
||
|
|
doc.text("#", 17, y + 6);
|
||
|
|
doc.text("Description", 28, y + 6);
|
||
|
|
doc.text("SKU", 100, y + 6);
|
||
|
|
doc.text("Qty", 130, y + 6, { align: "center" });
|
||
|
|
doc.text("Unit Price", 155, y + 6, { align: "right" });
|
||
|
|
doc.text("Total", 188, y + 6, { align: "right" });
|
||
|
|
y += 10;
|
||
|
|
|
||
|
|
doc.setFont("helvetica", "normal"); doc.setFontSize(8);
|
||
|
|
let subtotal = 0;
|
||
|
|
const lineCount = Math.max(quote.quantities?.length ?? 0, quote.items?.length ?? 0, quote.unitPrices?.length ?? 0);
|
||
|
|
for (let i = 0; i < lineCount; i++) {
|
||
|
|
const item = quote.items?.[i] || { itemName: "(item)", sku: "", sellingPrice: 0 };
|
||
|
|
const qty = quote.quantities?.[i] || 0;
|
||
|
|
const price = quote.unitPrices?.[i] || item.sellingPrice || 0;
|
||
|
|
const total = qty * price;
|
||
|
|
subtotal += total;
|
||
|
|
const rowH = 10;
|
||
|
|
if (i % 2 === 0) {
|
||
|
|
doc.setFillColor(250, 250, 250);
|
||
|
|
doc.rect(14, y, 182, rowH, "F");
|
||
|
|
}
|
||
|
|
doc.setTextColor(darkText);
|
||
|
|
doc.text(String(i + 1), 17, y + 5);
|
||
|
|
doc.text(String(item.itemName || "(item)").substring(0, 40), 28, y + 5);
|
||
|
|
doc.setTextColor(mediumGray); doc.setFontSize(7);
|
||
|
|
doc.text(item.sku || "—", 100, y + 5);
|
||
|
|
doc.setFontSize(8); doc.setTextColor(darkText);
|
||
|
|
doc.text(String(qty), 130, y + 5, { align: "center" });
|
||
|
|
doc.text(`R${price.toFixed(2)}`, 155, y + 5, { align: "right" });
|
||
|
|
doc.text(`R${total.toFixed(2)}`, 188, y + 5, { align: "right" });
|
||
|
|
y += rowH;
|
||
|
|
}
|
||
|
|
|
||
|
|
const vatAmount = subtotal * 0.15;
|
||
|
|
const totalWithVat = subtotal + vatAmount;
|
||
|
|
y += 5;
|
||
|
|
doc.setFillColor(lightGray);
|
||
|
|
doc.roundedRect(130, y, 66, 30, 2, 2, "F");
|
||
|
|
y += 7;
|
||
|
|
doc.setFont("helvetica", "normal"); doc.setFontSize(9); doc.setTextColor(mediumGray);
|
||
|
|
doc.text("Subtotal:", 135, y);
|
||
|
|
doc.setTextColor(darkText);
|
||
|
|
doc.text(`R${subtotal.toFixed(2)}`, 188, y, { align: "right" });
|
||
|
|
y += 8;
|
||
|
|
doc.setTextColor(mediumGray);
|
||
|
|
doc.text("Tax (VAT 15%):", 135, y);
|
||
|
|
doc.setTextColor(darkText);
|
||
|
|
doc.text(`R${vatAmount.toFixed(2)}`, 188, y, { align: "right" });
|
||
|
|
y += 8;
|
||
|
|
doc.setFillColor(brandBlack);
|
||
|
|
doc.roundedRect(130, y - 3, 66, 10, 2, 2, "F");
|
||
|
|
doc.setFont("helvetica", "bold"); doc.setFontSize(11); doc.setTextColor(255, 255, 255);
|
||
|
|
doc.text("TOTAL:", 135, y + 4);
|
||
|
|
doc.text(`R${totalWithVat.toFixed(2)}`, 188, y + 4, { align: "right" });
|
||
|
|
|
||
|
|
const out = `/tmp/${QUOTE_NUMBER.replace(/[^A-Za-z0-9_-]/g, "_")}.pdf`;
|
||
|
|
const bytes = Buffer.from(doc.output("arraybuffer"));
|
||
|
|
await fs.writeFile(out, bytes);
|
||
|
|
console.log(`\n✅ PDF written: ${out} (${bytes.length} bytes)`);
|
||
|
|
console.log(` Subtotal: R ${subtotal.toFixed(2)}`);
|
||
|
|
console.log(` VAT: R ${vatAmount.toFixed(2)}`);
|
||
|
|
console.log(` Total: R ${totalWithVat.toFixed(2)}`);
|
||
|
|
console.log(` Lines rendered: ${lineCount}`);
|