265 lines
7.7 KiB
TypeScript
265 lines
7.7 KiB
TypeScript
import {
|
|
createBrandedPdf,
|
|
addBrandedFooter,
|
|
COLORS,
|
|
} from "./pdf-template";
|
|
|
|
interface SupplierPoItem {
|
|
part_number: string;
|
|
component_name: string;
|
|
supplier_part_number?: string | null;
|
|
quantity: number;
|
|
unit_price: number;
|
|
}
|
|
|
|
interface SupplierPoPdfData {
|
|
po_number: string;
|
|
date: string;
|
|
status: string;
|
|
supplier_name: string;
|
|
supplier_code: string;
|
|
supplier_email?: string | null;
|
|
supplier_phone?: string | null;
|
|
supplier_address?: string | null;
|
|
supplier_contact?: string | null;
|
|
supplier_country?: string | null;
|
|
supplier_currency?: string | null;
|
|
shipping_cost?: number;
|
|
notes?: string | null;
|
|
items: SupplierPoItem[];
|
|
}
|
|
|
|
function makeCurrencyFormatter(currency: string) {
|
|
return (val: number): string =>
|
|
new Intl.NumberFormat("en-GB", {
|
|
style: "currency",
|
|
currency,
|
|
}).format(val);
|
|
}
|
|
|
|
export function generateSupplierPoPdf(data: SupplierPoPdfData) {
|
|
const { doc, contentY } = createBrandedPdf({
|
|
title: "PURCHASE ORDER",
|
|
documentNumber: data.po_number,
|
|
date: new Date(data.date).toLocaleDateString("en-GB"),
|
|
recipient: {
|
|
label: "SUPPLIER",
|
|
name: data.supplier_name,
|
|
code: data.supplier_code,
|
|
contact: data.supplier_contact,
|
|
address: data.supplier_address,
|
|
phone: data.supplier_phone,
|
|
email: data.supplier_email,
|
|
},
|
|
});
|
|
|
|
const pageWidth = doc.internal.pageSize.getWidth();
|
|
const margin = 14;
|
|
const currency = data.supplier_currency || "EUR";
|
|
const formatCurrency = makeCurrencyFormatter(currency);
|
|
const shippingCost = data.shipping_cost || 0;
|
|
const isPortugal = (data.supplier_country || "").toLowerCase().trim() === "portugal";
|
|
|
|
// --- Order Items Section ---
|
|
let y = contentY;
|
|
const contentWidth = pageWidth - margin * 2;
|
|
const rightEdge = pageWidth - margin;
|
|
|
|
const totalValue = data.items.reduce((sum, item) => sum + item.quantity * item.unit_price, 0);
|
|
const totalQty = data.items.reduce((sum, item) => sum + item.quantity, 0);
|
|
|
|
// Section header
|
|
doc.setFontSize(9);
|
|
doc.setFont("helvetica", "bold");
|
|
doc.setTextColor(...COLORS.primary);
|
|
doc.text("ORDER ITEMS", margin, y);
|
|
|
|
y += 5;
|
|
|
|
// Column headers -- no grid, just text labels
|
|
const colX = {
|
|
num: margin + 1,
|
|
part: margin + 10,
|
|
name: margin + 42,
|
|
ref: margin + 105,
|
|
qty: rightEdge - 54,
|
|
price: rightEdge - 30,
|
|
total: rightEdge,
|
|
};
|
|
|
|
doc.setFontSize(6);
|
|
doc.setFont("helvetica", "bold");
|
|
doc.setTextColor(...COLORS.muted);
|
|
doc.text("#", colX.num, y);
|
|
doc.text("PART NUMBER", colX.part, y);
|
|
doc.text("COMPONENT", colX.name, y);
|
|
doc.text("SUPPLIER REF", colX.ref, y);
|
|
doc.text("QTY", colX.qty, y, { align: "right" });
|
|
doc.text("UNIT PRICE", colX.price, y, { align: "right" });
|
|
doc.text("TOTAL", colX.total, y, { align: "right" });
|
|
|
|
y += 1.5;
|
|
doc.setDrawColor(...COLORS.border);
|
|
doc.setLineWidth(0.3);
|
|
doc.line(margin, y, rightEdge, y);
|
|
y += 3;
|
|
|
|
// Item rows -- clean card-like rows with left accent
|
|
const rowH = 9;
|
|
data.items.forEach((item, i) => {
|
|
const isAlt = i % 2 === 1;
|
|
const rowY = y;
|
|
|
|
// Subtle alternating background
|
|
if (isAlt) {
|
|
doc.setFillColor(250, 250, 252);
|
|
doc.rect(margin, rowY - 2.5, contentWidth, rowH, "F");
|
|
}
|
|
|
|
// Left accent bar (blue)
|
|
doc.setFillColor(...COLORS.accent);
|
|
doc.rect(margin, rowY - 2.5, 1.2, rowH, "F");
|
|
|
|
// Row number
|
|
doc.setFontSize(7);
|
|
doc.setFont("helvetica", "normal");
|
|
doc.setTextColor(...COLORS.muted);
|
|
doc.text(String(i + 1), colX.num + 2, rowY + 1.5);
|
|
|
|
// Part number -- monospaced feel
|
|
doc.setFontSize(7.5);
|
|
doc.setFont("courier", "bold");
|
|
doc.setTextColor(...COLORS.primary);
|
|
doc.text(item.part_number, colX.part, rowY + 1.5);
|
|
|
|
// Component name
|
|
doc.setFont("helvetica", "normal");
|
|
doc.setTextColor(...COLORS.secondary);
|
|
const nameMaxW = colX.ref - colX.name - 3;
|
|
const truncatedName = doc.getTextWidth(item.component_name) > nameMaxW
|
|
? item.component_name.substring(0, Math.floor(item.component_name.length * nameMaxW / doc.getTextWidth(item.component_name))) + "..."
|
|
: item.component_name;
|
|
doc.text(truncatedName, colX.name, rowY + 1.5);
|
|
|
|
// Supplier ref
|
|
doc.setFontSize(7);
|
|
doc.setFont("helvetica", "normal");
|
|
doc.setTextColor(...COLORS.muted);
|
|
doc.text(item.supplier_part_number || "-", colX.ref, rowY + 1.5);
|
|
|
|
// Quantity -- emphasized
|
|
doc.setFontSize(8);
|
|
doc.setFont("helvetica", "bold");
|
|
doc.setTextColor(...COLORS.primary);
|
|
doc.text(String(item.quantity), colX.qty, rowY + 1.5, { align: "right" });
|
|
|
|
// Unit price
|
|
doc.setFontSize(7.5);
|
|
doc.setFont("helvetica", "normal");
|
|
doc.setTextColor(...COLORS.secondary);
|
|
doc.text(formatCurrency(item.unit_price), colX.price, rowY + 1.5, { align: "right" });
|
|
|
|
// Line total
|
|
doc.setFont("helvetica", "bold");
|
|
doc.setTextColor(...COLORS.primary);
|
|
doc.text(formatCurrency(item.quantity * item.unit_price), colX.total, rowY + 1.5, { align: "right" });
|
|
|
|
y += rowH;
|
|
});
|
|
|
|
// Bottom separator
|
|
y += 1;
|
|
doc.setDrawColor(...COLORS.border);
|
|
doc.setLineWidth(0.3);
|
|
doc.line(margin, y, rightEdge, y);
|
|
|
|
// --- Summary section ---
|
|
y += 5;
|
|
|
|
const summaryW = 75;
|
|
const summaryX = rightEdge - summaryW;
|
|
const labelX = summaryX + 4;
|
|
const valueX = rightEdge - 4;
|
|
const lineSpacing = 6;
|
|
|
|
// Subtotal
|
|
doc.setFontSize(7.5);
|
|
doc.setFont("helvetica", "normal");
|
|
doc.setTextColor(...COLORS.muted);
|
|
doc.text("Subtotal", labelX, y);
|
|
doc.setFont("helvetica", "bold");
|
|
doc.setTextColor(...COLORS.primary);
|
|
doc.text(formatCurrency(totalValue), valueX, y, { align: "right" });
|
|
y += lineSpacing;
|
|
|
|
// Shipping
|
|
doc.setFont("helvetica", "normal");
|
|
doc.setTextColor(...COLORS.muted);
|
|
doc.text("Shipping", labelX, y);
|
|
doc.setFont("helvetica", "bold");
|
|
doc.setTextColor(...COLORS.primary);
|
|
doc.text(formatCurrency(shippingCost), valueX, y, { align: "right" });
|
|
y += lineSpacing;
|
|
|
|
// VAT for Portugal
|
|
let vatAmount = 0;
|
|
if (isPortugal) {
|
|
vatAmount = (totalValue + shippingCost) * 0.23;
|
|
doc.setFont("helvetica", "normal");
|
|
doc.setTextColor(...COLORS.muted);
|
|
doc.text("VAT (23%)", labelX, y);
|
|
doc.setFont("helvetica", "bold");
|
|
doc.setTextColor(...COLORS.primary);
|
|
doc.text(formatCurrency(vatAmount), valueX, y, { align: "right" });
|
|
y += lineSpacing;
|
|
}
|
|
|
|
// Separator line
|
|
y += 1;
|
|
doc.setDrawColor(...COLORS.border);
|
|
doc.setLineWidth(0.3);
|
|
doc.line(summaryX, y, rightEdge, y);
|
|
y += 4;
|
|
|
|
// Grand total box
|
|
const grandTotal = totalValue + shippingCost + vatAmount;
|
|
const totalBoxW = summaryW;
|
|
const totalBoxX = summaryX;
|
|
const totalBoxY = y - 3;
|
|
|
|
doc.setFillColor(...COLORS.accent);
|
|
doc.roundedRect(totalBoxX, totalBoxY, totalBoxW, 12, 2, 2, "F");
|
|
|
|
doc.setFontSize(6.5);
|
|
doc.setFont("helvetica", "bold");
|
|
doc.setTextColor(220, 230, 245);
|
|
doc.text("TOTAL", totalBoxX + 5, totalBoxY + 5);
|
|
doc.setFontSize(11);
|
|
doc.setFont("helvetica", "bold");
|
|
doc.setTextColor(255, 255, 255);
|
|
doc.text(formatCurrency(grandTotal), totalBoxX + totalBoxW - 5, totalBoxY + 9, { align: "right" });
|
|
|
|
const finalY = totalBoxY + 14;
|
|
|
|
// --- Notes ---
|
|
if (data.notes) {
|
|
const noteY = totalBoxY + 18;
|
|
doc.setFillColor(...COLORS.bgLight);
|
|
doc.setDrawColor(...COLORS.border);
|
|
doc.setLineWidth(0.2);
|
|
doc.roundedRect(margin, noteY, contentWidth, 16, 1.5, 1.5, "FD");
|
|
doc.setFontSize(6.5);
|
|
doc.setFont("helvetica", "bold");
|
|
doc.setTextColor(...COLORS.accent);
|
|
doc.text("NOTES", margin + 4, noteY + 5);
|
|
doc.setFontSize(7.5);
|
|
doc.setFont("helvetica", "normal");
|
|
doc.setTextColor(...COLORS.secondary);
|
|
const noteLines = doc.splitTextToSize(data.notes, contentWidth - 8);
|
|
doc.text(noteLines, margin + 4, noteY + 11);
|
|
}
|
|
|
|
addBrandedFooter(doc);
|
|
return doc;
|
|
}
|