#!/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 */ 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}`);