import { newPDF, drawHeader, drawText, drawTable, moveDown, COLORS, type PDFContext, } from "./shared"; import type { Customer, DeliveryNote, FuelSettings } from "../types"; import { formatCurrency, formatDate, formatNumber } from "../utils"; export interface StatementInput { customer: Customer; notes: DeliveryNote[]; startDate: string; endDate: string; includePricing: boolean; companySettings: FuelSettings | null; } export async function generateCustomerStatement(input: StatementInput): Promise { const ctx = await newPDF(); drawHeader( ctx, "Delivery Statement", `${formatDate(input.startDate)} to ${formatDate(input.endDate)}`, ); // Customer info block — two columns at the same y, then a few lines under the left column const c = input.customer; const infoTopY = ctx.y; drawText(ctx, "Bill to", { x: 40, y: infoTopY, size: 8, color: COLORS.muted }); drawText(ctx, "Statement period", { x: 320, y: infoTopY, size: 8, color: COLORS.muted }); const nameY = infoTopY - 14; drawText(ctx, c.name, { x: 40, y: nameY, size: 12, bold: true }); drawText(ctx, `${formatDate(input.startDate)} to ${formatDate(input.endDate)}`, { x: 320, y: nameY, size: 10, bold: true }); let y = nameY - 14; if (c.email) { drawText(ctx, c.email, { x: 40, y, size: 9, color: COLORS.muted }); y -= 12; } if (c.contact_number) { drawText(ctx, c.contact_number, { x: 40, y, size: 9, color: COLORS.muted }); y -= 12; } if (c.address) { drawText(ctx, c.address, { x: 40, y, size: 9, color: COLORS.muted }); y -= 12; } if (c.vat_number) { drawText(ctx, `VAT: ${c.vat_number}`, { x: 40, y, size: 9, color: COLORS.muted }); y -= 12; } ctx.y = y - 8; // resume below the info block // Summary card — three columns, label at top, value below const totalQty = input.notes.reduce((a, n) => a + (n.qty_litres_supplied || 0), 0); const totalValue = input.notes.reduce((a, n) => a + (n.total_value || 0), 0); const cardTop = ctx.y - 4; const cardHeight = 56; const cardBottom = cardTop - cardHeight; ctx.page.drawRectangle({ x: 40, y: cardBottom, width: 515, height: cardHeight, color: COLORS.bgAlt, borderColor: COLORS.border, borderWidth: 0.5, }); // Column 1: Records drawText(ctx, "Records", { x: 56, y: cardTop - 14, size: 8, color: COLORS.muted }); drawText(ctx, String(input.notes.length), { x: 56, y: cardTop - 36, size: 16, bold: true }); // Column 2: Total quantity drawText(ctx, "Total quantity", { x: 220, y: cardTop - 14, size: 8, color: COLORS.muted }); drawText(ctx, `${formatNumber(totalQty)} L`, { x: 220, y: cardTop - 36, size: 16, bold: true }); // Column 3: Total value drawText(ctx, "Total value", { x: 400, y: cardTop - 14, size: 8, color: COLORS.muted }); drawText(ctx, formatCurrency(totalValue), { x: 400, y: cardTop - 36, size: 16, bold: true }); ctx.y = cardBottom - 12; // Detailed table const columns: { header: string; width: number; align?: "left" | "right" | "center"; format?: (v: any) => string }[] = [ { header: "Date", width: 70, format: (v: string) => formatDate(v) }, { header: "Note #", width: 80 }, { header: "Vehicle", width: 90 }, { header: "Qty (L)", width: 60, align: "right", format: (v: number) => formatNumber(v) }, ]; if (input.includePricing) { columns.push( { header: "Price/L", width: 70, align: "right", format: (v: number) => formatCurrency(v) }, { header: "Total", width: 80, align: "right", format: (v: number) => formatCurrency(v) }, ); } const rows = input.notes.map((n) => { const row: any[] = [n.date, n.number, n.vehicle_registration || "—", n.qty_litres_supplied]; if (input.includePricing) { row.push(n.price_per_litre, n.total_value); } return row; }); drawTable(ctx, columns, rows); // Totals row — single line, right-aligned to match the last column if (input.notes.length > 0) { moveDown(ctx, 6); const lineY = ctx.y + 4; ctx.page.drawLine({ start: { x: 40, y: lineY }, end: { x: 555, y: lineY }, thickness: 0.5, color: COLORS.border }); moveDown(ctx, 12); drawText(ctx, "Total", { x: 40, size: 10, bold: true }); const tableRight = 40 + columns.reduce((a, c) => a + c.width, 0); const totalQtyStr = formatNumber(totalQty) + " L"; const qtyW = ctx.font.widthOfTextAtSize(totalQtyStr, 10); const qtyColStart = 40 + columns.slice(0, 3).reduce((a, c) => a + c.width, 0); drawText(ctx, totalQtyStr, { x: qtyColStart + columns[3].width - 8 - qtyW, size: 10, bold: true }); if (input.includePricing) { const totalValStr = formatCurrency(totalValue); const valW = ctx.font.widthOfTextAtSize(totalValStr, 10); drawText(ctx, totalValStr, { x: tableRight - 8 - valW, size: 10, bold: true }); } } // Footer message moveDown(ctx, 30); drawText(ctx, "Thank you for your business.", { size: 9, color: COLORS.muted }); return finalizeWithPageNumbers(ctx); } async function finalizeWithPageNumbers(ctx: PDFContext): Promise { const pages = ctx.doc.getPages(); pages.forEach((p, i) => { const stamp = `Generated ${new Date().toLocaleString("en-ZA")} • Page ${i + 1} of ${pages.length}`; const w = ctx.font.widthOfTextAtSize(stamp, 8); p.drawText(stamp, { x: 555 - w, y: 30, size: 8, font: ctx.font, color: COLORS.muted }); }); return ctx.doc.save(); }