import PdfPrinter from "pdfmake/src/Printer"; import { readFileSync } from "node:fs"; import { join } from "node:path"; export interface QuoteLineItem { qty: number; description: string; unitPrice: number; narrative?: string; } export interface CustomerInfo { name: string; company?: string; attention?: string; address: string[]; } export interface CustomerContact { email?: string; phone?: string; } export interface QuoteDetails { quoteNumber: string; date: string; description: string; } export interface TaxConfig { rate: number; label: string; } export interface SalesRepInfo { name: string; email?: string; } export interface QuoteMetadata { quoteId?: string; createdById?: string; createdByName?: string; createdByEmail?: string; createdAt?: string; downloadedAt?: string; downloadedById?: string; downloadedByName?: string; downloadedByEmail?: string; } export interface QuoteData { customer: CustomerInfo; contact: CustomerContact; quote: QuoteDetails; lineItems: QuoteLineItem[]; tax: TaxConfig; salesRep?: SalesRepInfo; quoteNarrative?: string; isPreview?: boolean; showLineItemPricing?: boolean; metadata?: QuoteMetadata; } export interface QuoteTheme { brandPrimary: string; brandDark: string; brandLight: string; accent: string; headerBg: string; footerBg: string; } const DEFAULT_THEME: QuoteTheme = { brandPrimary: "#8B5E0B", brandDark: "#5C3D07", brandLight: "#F5EDE0", accent: "#C67F17", headerBg: "#2D2317", footerBg: "#F5EDE0", }; const SLATE = "#3A3A3A"; const SLATE_MID = "#636363"; const SLATE_LIGHT = "#8E8E8E"; const WHITE = "#FFFFFF"; const ROW_ALT = "#FAF7F2"; const DIVIDER = "#D4C5A9"; const PAGE_H = 792; const PAGE_W = 612; const MARGIN_L = 40; const MARGIN_R = 40; const MARGIN_TOP = 26; const MARGIN_BOTTOM = 65; const CONTENT_W = PAGE_W - MARGIN_L - MARGIN_R; const DEFAULT_DISCLAIMER = "Prices valid for 30 days from quote date. Taxes invoiced per jurisdiction regardless of presence on this quote."; const COMPANY = { name: "Total Tech Solutions LLC", contactPerson: "Courtney Stevens", address: ["PO Box 331", "Murray, KY 42071"], phone: "(270) 761-8324", email: "courtney.stevens@totaltech.net", licenseInfo: "Licensed in Kentucky & Tennessee · TN License #2173", } as const; const DEFAULT_LOGO_PATH = join(process.cwd(), "logo.png"); const fontDir = join(process.cwd(), "node_modules/pdfmake/build/fonts/Roboto"); const fonts = { Roboto: { normal: join(fontDir, "Roboto-Regular.ttf"), bold: join(fontDir, "Roboto-Medium.ttf"), italics: join(fontDir, "Roboto-Italic.ttf"), bolditalics: join(fontDir, "Roboto-MediumItalic.ttf"), }, }; const printer = new PdfPrinter(fonts as never); const fmt = (n: number) => "$" + n.toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ","); const hr = (color = DIVIDER, weight = 0.75) => ({ canvas: [ { type: "line", x1: 0, y1: 0, x2: CONTENT_W, y2: 0, lineWidth: weight, lineColor: color, }, ], }); function loadLogoDataUrl(logoPath: string): string | null { try { const raw = readFileSync(logoPath); const ext = logoPath.toLowerCase().endsWith(".png") ? "png" : "jpeg"; return `data:image/${ext};base64,${raw.toString("base64")}`; } catch { return null; } } export async function generateQuote( data: QuoteData, theme: Partial = {}, logoPath = DEFAULT_LOGO_PATH, ): Promise { const t: QuoteTheme = { ...DEFAULT_THEME, ...theme }; const subTotal = data.lineItems.reduce( (sum, item) => sum + item.qty * item.unitPrice, 0, ); const taxAmount = subTotal * data.tax.rate; const total = subTotal + taxAmount; const logoDataUrl = loadLogoDataUrl(logoPath); const showPricing = data.showLineItemPricing ?? false; const tableHeader = [ { text: "Qty", style: "thCell", alignment: "center" }, { text: "Description", style: "thCell" }, ...(showPricing ? [ { text: "Unit Price", style: "thCell", alignment: "right" }, { text: "Total", style: "thCell", alignment: "right" }, ] : []), ]; const colCount = showPricing ? 4 : 2; const tableRows: Record[][] = []; for (const item of data.lineItems) { // Build the description cell — stack description + narrative so they // are a single cell and pdfmake never splits them across pages. const descriptionCell: Record = item.narrative ? { stack: [ { text: item.description, style: "tdCell" }, { text: item.narrative, style: "narrative", margin: [0, 2, 8, 0], }, ], } : { text: item.description, style: "tdCell" }; tableRows.push([ { text: String(item.qty), style: "tdCell", alignment: "center" }, descriptionCell, ...(showPricing ? [ { text: fmt(item.unitPrice), style: "tdCell", alignment: "right", noWrap: true, }, { text: fmt(item.qty * item.unitPrice), style: "tdCell", alignment: "right", noWrap: true, }, ] : []), ]); } const headerImage = logoDataUrl ? { image: logoDataUrl, width: 200 } : { stack: [{ text: COMPANY.name, style: "companyName" }], width: 200, }; const docDefinition = { pageSize: "LETTER" as const, pageMargins: [MARGIN_L, MARGIN_TOP, MARGIN_R, MARGIN_BOTTOM] as [ number, number, number, number, ], info: { title: `Quote ${data.quote.quoteNumber}`, author: data.metadata?.createdByName ?? COMPANY.name, subject: data.quote.description, creator: COMPANY.name, producer: COMPANY.name, keywords: [ data.metadata?.quoteId ? `quoteId:${data.metadata.quoteId}` : null, data.metadata?.createdById ? `createdById:${data.metadata.createdById}` : null, data.metadata?.createdByEmail ? `createdByEmail:${data.metadata.createdByEmail}` : null, data.metadata?.createdAt ? `createdAt:${data.metadata.createdAt}` : null, data.metadata?.downloadedAt ? `downloadedAt:${data.metadata.downloadedAt}` : null, data.metadata?.downloadedById ? `downloadedById:${data.metadata.downloadedById}` : null, data.metadata?.downloadedByName ? `downloadedByName:${data.metadata.downloadedByName}` : null, data.metadata?.downloadedByEmail ? `downloadedByEmail:${data.metadata.downloadedByEmail}` : null, data.isPreview ? "preview:true" : null, ] .filter(Boolean) .join("; "), }, defaultStyle: { font: "Roboto", fontSize: 9.5, color: SLATE, lineHeight: 1.3, }, styles: { companyName: { fontSize: 18, bold: true, color: t.brandDark }, quoteLabel: { fontSize: 24, color: t.accent, bold: true, opacity: 0.12 }, sectionTitle: { fontSize: 8.5, bold: true, color: t.brandPrimary, characterSpacing: 1.2, }, sectionBody: { fontSize: 9, color: SLATE }, sectionMuted: { fontSize: 8.5, color: SLATE_MID }, infoLabel: { fontSize: 8, bold: true, color: SLATE_LIGHT, characterSpacing: 0.5, }, infoValue: { fontSize: 10, bold: true, color: t.brandDark }, contactLabel: { fontSize: 8, bold: true, color: SLATE_LIGHT }, contactValue: { fontSize: 9, color: SLATE }, thCell: { fontSize: 8.5, bold: true, color: WHITE, characterSpacing: 0.5, }, tdCell: { fontSize: 9, color: SLATE }, narrative: { fontSize: 8, color: SLATE_MID, italics: true, lineHeight: 1.2, }, totalsLabel: { fontSize: 9, color: SLATE_MID }, totalsValue: { fontSize: 9, color: SLATE, bold: true }, totalFinalLabel: { fontSize: 11, bold: true, color: WHITE }, totalFinalValue: { fontSize: 12, bold: true, color: t.brandDark }, footerText: { fontSize: 7.5, color: SLATE_MID }, footerBold: { fontSize: 7.5, color: t.brandPrimary, bold: true }, disclaimer: { fontSize: 7, color: SLATE_LIGHT, italics: true }, }, ...(data.isPreview ? { watermark: { text: "PREVIEW", color: t.brandDark, opacity: 0.15, bold: true, }, } : {}), background: () => ({ canvas: [ { type: "rect", x: 0, y: 0, w: PAGE_W, h: 6, color: t.accent }, { type: "rect", x: 0, y: 6, w: 4, h: 786, color: t.brandLight }, ], }), content: [ { margin: [0, 4, 0, 0], columns: [ headerImage, { stack: [ { text: COMPANY.name, style: "companyName", alignment: "right" }, { text: "QUOTE", style: "quoteLabel", alignment: "right", margin: [0, -4, 0, 0], }, ], width: "*", }, ], }, { ...hr(t.accent, 1.5), margin: [0, 8, 0, 0] }, { margin: [0, 7, 0, 7], columns: [ { width: "auto", stack: [ { text: "QUOTE NUMBER", style: "infoLabel" }, { text: data.quote.quoteNumber, style: "infoValue", margin: [0, 2, 0, 0], }, ], }, { width: "auto", margin: [30, 0, 0, 0], stack: [ { text: "DATE", style: "infoLabel" }, { text: data.quote.date, style: "infoValue", margin: [0, 2, 0, 0], }, ], }, { width: "*", margin: [30, 0, 0, 0], stack: [ { text: "DESCRIPTION", style: "infoLabel" }, { text: data.quote.description, style: "infoValue", margin: [0, 2, 0, 0], }, ], }, ], }, { ...hr(), margin: [0, 0, 0, 10] }, { columns: [ { width: 155, stack: [ { text: "FROM", style: "sectionTitle", margin: [0, 0, 0, 6] }, { text: data.salesRep?.name ?? COMPANY.contactPerson, style: "sectionBody", bold: true, }, { text: COMPANY.name, style: "sectionMuted", margin: [0, 2, 0, 0], }, ...COMPANY.address.map((line) => ({ text: line, style: "sectionMuted", })), { text: COMPANY.phone, style: "sectionBody", margin: [0, 4, 0, 0], }, { text: data.salesRep?.email ?? COMPANY.email, style: "sectionMuted", margin: [0, 1, 0, 0], }, ], }, { width: 175, margin: [25, 0, 0, 0], stack: [ { text: "PREPARED FOR", style: "sectionTitle", margin: [0, 0, 0, 6], }, { text: data.customer.name, style: "sectionBody", bold: true }, ...(data.customer.company ? [ { text: data.customer.company, style: "sectionMuted", margin: [0, 2, 0, 0], }, ] : []), ...(data.customer.attention ? [{ text: data.customer.attention, style: "sectionMuted" }] : []), ...data.customer.address.map((line) => ({ text: line, style: "sectionMuted", })), ], }, ...(data.contact.email || data.contact.phone ? [ { width: "*" as const, margin: [20, 0, 0, 0] as [number, number, number, number], stack: [ { text: "CONTACT", style: "sectionTitle", margin: [0, 0, 0, 6], }, ...(data.contact.email ? [ { columns: [ { text: "Email", style: "contactLabel", width: 40, }, { text: data.contact.email, style: "contactValue", width: "*", }, ], }, ] : []), ...(data.contact.phone ? [ { columns: [ { text: "Mobile", style: "contactLabel", width: 40, }, { text: data.contact.phone, style: "contactValue", width: "*", }, ], margin: [0, 4, 0, 0], }, ] : []), ], }, ] : []), ], }, { ...hr(), margin: [0, 10, 0, 0] }, ...(data.quoteNarrative ? [ { margin: [0, 8, 0, 6] as [number, number, number, number], table: { widths: [2, "*"], body: [ [ { text: "", fillColor: t.accent, border: [false, false, false, false], }, { text: data.quoteNarrative, fontSize: 9, color: SLATE_MID, italics: true, lineHeight: 1.4, margin: [8, 6, 8, 6], fillColor: ROW_ALT, border: [false, false, false, false], }, ], ], }, layout: { hLineWidth: () => 0, vLineWidth: () => 0, paddingLeft: () => 0, paddingRight: () => 0, paddingTop: () => 0, paddingBottom: () => 0, }, }, ] : []), { margin: [0, 10, 0, 0], table: { headerRows: 1, dontBreakRows: true, widths: showPricing ? [40, "*", 75, 75] : [40, "*"], body: [tableHeader, ...tableRows], }, layout: { fillColor: (rowIndex: number) => { if (rowIndex === 0) return t.headerBg; return rowIndex % 2 === 0 ? ROW_ALT : null; }, hLineWidth: (i: number, node: { table: { body: unknown[] } }) => { if (i === 0 || i === 1) return 0; if (i === node.table.body.length) return 1; return 0.5; }, vLineWidth: () => 0, hLineColor: (i: number, node: { table: { body: unknown[] } }) => i === node.table.body.length ? t.headerBg : "#E8E0D0", paddingLeft: (col: number) => (col === 0 ? 6 : 8), paddingRight: () => 8, paddingTop: () => 4, paddingBottom: () => 4, }, }, { unbreakable: true, stack: [ { margin: [0, 6, 0, 0], columns: [ { width: "*", text: "" }, { width: 250, table: { widths: ["*", 110], body: [ [ { text: "Subtotal", style: "totalsLabel", margin: [0, 5, 0, 5], border: [false, false, false, true], }, { text: fmt(subTotal), style: "totalsValue", alignment: "right", noWrap: true, margin: [0, 5, 0, 5], border: [false, false, false, true], }, ], [ { text: data.tax.label, style: "totalsLabel", margin: [0, 5, 0, 5], border: [false, false, false, true], }, { text: fmt(taxAmount), style: "totalsValue", alignment: "right", noWrap: true, margin: [0, 5, 0, 5], border: [false, false, false, true], }, ], [ { text: "TOTAL", style: "totalFinalLabel", fillColor: t.headerBg, margin: [10, 8, 6, 8], border: [false, false, false, false], }, { text: fmt(total), style: "totalFinalValue", alignment: "right", noWrap: true, fillColor: t.brandLight, margin: [6, 7, 8, 7], border: [false, false, false, false], }, ], ], }, layout: { hLineWidth: (i: number) => (i >= 1 && i <= 2 ? 0.5 : 0), vLineWidth: () => 0, hLineColor: () => "#E0D6C6", }, }, ], }, { margin: [0, 40, 0, 0], columns: [ { width: "50%", stack: [ { canvas: [ { type: "line", x1: 0, y1: 0, x2: 220, y2: 0, lineWidth: 0.75, lineColor: "#999", }, ], }, { text: "Authorized Signature", fontSize: 7, color: "#888", margin: [0, 3, 0, 0], }, ], }, { width: "50%", stack: [ { canvas: [ { type: "line", x1: 0, y1: 0, x2: 160, y2: 0, lineWidth: 0.75, lineColor: "#999", }, ], }, { text: "Date", fontSize: 7, color: "#888", margin: [0, 3, 0, 0], }, ], }, ], }, ], }, ], footer: (currentPage: number, pageCount: number) => ({ margin: [0, 0, 0, 0], stack: [ { canvas: [ { type: "rect", x: 0, y: 0, w: PAGE_W, h: 44, color: t.footerBg }, ], }, { margin: [MARGIN_L, -38, MARGIN_R, 0], columns: [ { width: "*", stack: [ { text: [ { text: COMPANY.name, style: "footerBold" }, { text: ` · ${COMPANY.licenseInfo}`, style: "footerText", }, ], }, ], }, { width: "auto", text: `Page ${currentPage} of ${pageCount}`, style: "footerText", alignment: "right", }, ], }, { margin: [MARGIN_L, 4, MARGIN_R, 0], text: DEFAULT_DISCLAIMER, style: "disclaimer", }, ], }), }; const maybeDoc = printer.createPdfKitDocument(docDefinition as never) as any; const pdfDoc = maybeDoc && typeof maybeDoc.then === "function" ? await maybeDoc : maybeDoc; if (!pdfDoc || typeof pdfDoc.on !== "function") { throw new Error("Failed to initialize PDF document stream"); } return await new Promise((resolve, reject) => { try { const chunks: Buffer[] = []; pdfDoc.on("data", (chunk: Buffer) => chunks.push(chunk)); pdfDoc.on("end", () => resolve(Buffer.concat(chunks))); pdfDoc.on("error", reject); if (typeof pdfDoc.end === "function") { pdfDoc.end(); } else { reject(new Error("PDF document stream does not support end()")); } } catch (err) { reject(err); } }); }