import jsPDF from "jspdf"; // Cropped & compressed logo (3KB, whitespace trimmed) export const EACH_LOGO_BASE64 = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAZAAAACJCAMAAAA1z54ZAAAAIVBMVEVMaXEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAC+JJ50AAAAC3RSTlMA/hDG3ImnSCnvaOv8ckcAAAAJcEhZcwAALiMAAC4jAXilP3YAAAAZdEVYdFNvZnR3YXJlAEFkb2JlIEltYWdlUmVhZHlxyWU8AAAMP0lEQVR42u1d2ZakIAwt2eX/P3hAdkRLJcHqOfAwS7dLyAUSrkn4fHqa4uuycvUBakTTZaHy8xtNcs4vyyKt5Pp5zzlbVtGtSL0uW9MwGlDUPU6QH4DDC0MvKYl4yelDlUqvSN6JxxIaCCKKhcfR9xFRXkXLKq/jsSzskeQyKpKDyGykhli1xAIjF8jiGVV8Rcc8SS6evIyl+3vW60wMCA2q7HHs7SmiM2G+Tn+SRuajoZm/THQITbPnUFgdLOplQMSdvslcctk3slcgQBgwIG97Wrf6VgCi+9Bf5gwBBuS9GULof2xDxJ1l/TdsiGTL8r96WYTfG/O/4GUVIgPtQ9Yf2YeofKxdUXHS6Fv7kLCNBd2pZ4D8gMu73hkc6uWduqNMVm24LMooFJdlJx2jG8jyBygTbSRZmb6jkg4uS3TASUTXaDhszCHBO7dHIPTcplql1C070OOKyOfzw1tzcLsr/RJsV671Jbf3+Vh7DxBnzZlE8TR5+Id+BY8w1sjnzwDiV1gEgnxz5lWQ7RWz3jPWXgIkWnMc30YkJ1K+Zc0fjrVXAMGy5pGs0Gmk8nes+eOx9gYg3lnGUZXKdlVqPHnSPdbGA0LQrPl+VtDRZr3fcxwOiKcTsD53l3ZDj92KQIy10YDoy9+Xn8tDSxJF/Q1r/gogtyIwHm9CdGNPMo660v1TfBggiM5uNiXy3bkcZtYVkOc4EBBUZzcz6aLBa+FbcyjPcRwgHXTC5y6veIIQprPL1OfvAILr7B6uUEMYRsixNggQPOqqzSseWnk86grIcxwDCL41r3jFIz/4V53dsYCMsObR8aSnO8Wfo67eAATrQ9TnjFc84FJ+jroaDwgZYs1PuETMrQiC54gNyCBrfjYX8BhGjLGGDMgga35qLbAYRpyxhgoIPnV1xZ9CYhiRxhomIHkkzGdMGK2+uDv5QWuODgggnfCIV0Q163gfPdEAGUJdXWStoLcimJ4jFiAc+UNU25mSt7HqiKHG8RxxABnp7F5Yl2AZRtyxhgLISGf3kuUW4DnwaJ4jAiCnDgghZBiveE5y9cdQ/xFATqgrYiPzmVCDeEU4hlFxIYQmI2jSEhAlFekD5MwBCdkoGOPrCz/SxzD6LC+qB3iOOSC2YospAUM6ADlzQLLkTj2GVwTaipRZd8g0aQZIyJW8tZUrATl1QDhicuzXGdDBMBbJyuieYwIk6YuSy60AxC9JTBLVagwvn/+7jehgGPPMZnzPMQKSVYBZ2I2WAAkJh+u6fG18DK8IsRXJc40HfGWLgOjlceP7ug6DAbmwz3jMMJIFqKTLTUBELyBFAYBvTQ/iFSuz/niH88IMKQC5vmKtSUrvpJsKc7TZGFoJkktc1eOtCC0GEhkFSO7b0WdGPVAm/Ku3IpDjFUEZRj3O580Bkc8mZun2nhM9EXPgbySNTYbSpu4kATLrxeLBh7m94lEFmHqnfsoreiIImjvZ2WvCW1I8ZhjjWjzig3QCJG2kZQ+XdTpJiBaCS3ReUTSnegfDKAWzlT/0im/Zs516YM5kJ9vLl7Hk+07RvL0B7WIYHUk9IGqjIBfNyqtl//cQOfbzVM2K5H4qhY5hRB9tKB+oyMgPuJZXXMkB91SYQ5As6fAxVP0lQOIuccQk4S1b0SbNQGIYCe4kwQpyCEFAcny84nERTKAYRvn3ghzy0CUyOl6xWLJQ0qn+YhjQsEi5xu6CHVadBYthxIs6Q43tHeC3twb9MekAl06FtiTjRr/j++1NhkofIQKZToU02rDzQ7BDtNrxitJWwDSs847IhIxhxBlt6BlUuJPkMHjBfEUmng0S9aZF/XJGwoAcQ0y//YuV9ogQrCxphNE2IgsXz28n33JxHCJZDWMNnCUNPtqG5Kmj+e3f1eu8oYQIeJY09GgbVMkBaZJc4UIqRMCzpIGJu1G1TlD89h2veIxI3J5ipFNBjrZx1YAQ/PaLTiwvEKEIcXqAo21gvSwFHTp+2R4UiKBkScMRdy9UlAObJNeJEJ6t8jgFm8CIu79cc/HGnkJniCDVYQQabaOrkgL67beGeoYIVsEmmNE2vm4v2Af3e7yUjh8Q8Qo2QdQlfa2ydb/fftNdkjGoGK8OI8AH91dqv4P47bc3FM4T0le3Ly9RKW+ejtDnt9+3zRERzJLwvaPtpfND+j+Bkgc1fhwiHJxhhCTu3jphp3uSPFKqSxPjm1lXv1k19r0zqDr99mfLjkt05MgnvfSMthdPaav8doJr0gtEKHZJeP2YSnkRkPwT6K2DJU1BiDxz69Ew2M6eFHhhfPlok1zUySrfDpYkfQdLyl6/nd/JFIoqlf2HUyPGuoaqKJLeWL96j171L32+zSqPU76ESO8BvhUiH+xJcic3P+ub6qtxAHR69xVuSnRrkwi0fODTmhzkTxzfXU0Sfv0o6Ocseh7aOKIu/FXsf+OA+zLvmN6qPCL7Ry7uWRZkaN+KIiBAgLA7o+DpejMOkHt9k519K9KF5wx5v28caIbwOzakWGfJj9uQsm/ijlF+04ZkdvqCGByg8ggd42VV2EvsvsF4WQWy+o6dpKR/CIgxR3lffFXq27M9loSqkuM3mNe+6wSpKemuhTzilBlxhxQIW6SnO3UJ1S9T9pOKq1yWKaxAe3koU1OCXibPOlctuq4r1WP6NrBff7gRRaYSZpttttlmm2222WabbbbZZpttttlmm2222WabbbbZZpttttlmm2222Wb7I42YZLpwJJvQKp0HlY5tS+frSM7LTDrlookI59UZPNqlTpob8ibrZ8c8PsWrpt3t7vH64PGVnLoKbVKxb1m+YN7hdIvanSEkq0Apd0Vbzni3eTjdP7yhHp30LAotm+yUNT8jj4dYypWlJ68hKE/UwZXaxZGqXR0UtvqjYdesuadrm7oZ3rj6PDDpL1zC38LdrmOsti7jfFWI+WQN8WNAaOzFGvIFiw6nW+QuaFRUkbbuirac4W4Tg8iKh+tPUz1kXWNuUBDRqMKGddKio+YC6XRWxlBqH28uFnNyV/6rCAgVpcoiIOYQwfzEPit8EXIZHu0vsALESxMgrHq8B6SSUxW94eZFZPeessM2DFTfASQIV8oZfsfKe4I6N/XkiBDGvDDZNDSicHOpaL2V1iUThPuB+UuuZUXdAMinVFkGyJeOlu8q/5cAscNd7y6r5SzUWv3S/bfu8Cb6HUCaD5dholThuf7n9qWFevwMUWVehf2vrh7hLrEK+FTh4DIIaaZlQiQDpFTZMSC1EnnR8UNA7ON5dZn5OWlp14/C6hwSP6d4nQ3gT4QEAETXvSVOnp16PCD2NlKKuHvEJjbZZcT46zYhTRB6XHRyQAqVHQJS66mKoT0GxNoeXgGyk1OpY+Q/TZ15SUEA4XWChx/ZblpmiLRnSFu+9oU5IHZpTNVCM0BylV0H5Gz+FIDkjw/rDz1N8qByFzDdAGRBBYSkdVJHRIJRp6ZsGukHxCZ7eERKQGwpJV4BQk3Kgm/UWuAOQOzjxTkgZH90qK/IwdUrgKwZIGlE5V5WSENjwu4ongFiHQKmGoAklUVAWH72NIkyPgLE2i/RBCQk2Ky03GNpv1sQwQV9FZA4YAMglYhMPQXE/mtz42pAosowliyXRLU5ujUgYQN6vIZJl3D25pKVEEmAlGss38lHtp0n+Q6I3fLJBiBBZdcBKXfEXwCxk9xtn5pGnaz0C9Q9Rp33GfWoniNAHrq9BSJ7QLzK4N3elKxIyRW39yPL6koBEN4cIHtA+BNAjtxe8SkQ8YCQkuux0u83hvx8YyiLNFzZAMSpzC0EVzaGYrkzQzb7Rcn9jeGR4g83hjV0ZwPnwsYwH7DCCNMwDdvVtDybOVIn9cl1tKFMbWxoA5BNZTl1shOSNKiTy4BYKQxNFKiTT0P8tCTnMgV3oB4PB9SJGcXFmbzlbGwCYvSsKl5G7gDZBqzrUEm1mLmjtz+ZiH5p9FqNzY7eqnD2vzG6bcFQ3fICRCjYYEnK1Fbuf0b3j74ByMYaqr2cSfzQ8dg1GjOciw7bWzwtmAvq5LQkUbqwIk+bgJS3JHmqdcimUTt0s+sjz2lK763RFU72VYpUYMCkB/vBoatUbClcMjAR9fHswvVfi7z5NGPF2f7R8caCeaTnjy/lZBVhTuIv1yzpOOuwuSUkM6uGnLlqdnIWmlAhgbp8eHipquQnPPwgXW/eoD6ff0BzZamQrjexAAAAAElFTkSuQmCC"; // Company details export const COMPANY = { name: "EACH, Lda", address: "Alameda Silva Porto 11", postalCity: "1685 - 636 Famoes", vat: "PT513346325", }; // Professional colour palette -- warm neutral tones export const COLORS = { primary: [23, 23, 23] as [number, number, number], // near-black secondary: [64, 64, 64] as [number, number, number], // dark grey muted: [115, 115, 115] as [number, number, number], // medium grey border: [210, 210, 210] as [number, number, number], // light grey bgLight: [247, 247, 247] as [number, number, number], // off-white white: [255, 255, 255] as [number, number, number], accent: [230, 126, 34] as [number, number, number], // warm orange }; export interface RecipientInfo { label: string; name: string; code?: string; contact?: string | null; address?: string | null; phone?: string | null; email?: string | null; } export interface TemplateOptions { title: string; documentNumber: string; date: string; recipient: RecipientInfo; } /** * Creates a jsPDF document with a unified professional branded layout. * Returns the doc and the Y position where content can start. */ export function createBrandedPdf( options: TemplateOptions ): { doc: jsPDF; contentY: number } { const doc = new jsPDF({ format: "a4" }); const pageWidth = doc.internal.pageSize.getWidth(); const margin = 14; const contentWidth = pageWidth - margin * 2; const rightEdge = pageWidth - margin; // ============================================= // TOP SECTION: Logo left | Company info right // ============================================= // Company info (top-right) -- 4 rows of 3.5mm line height starting at Y=10 const companyStartY = 10; const lineH = 3.8; doc.setFontSize(7.5); doc.setFont("helvetica", "bold"); doc.setTextColor(...COLORS.primary); doc.text(COMPANY.name, rightEdge, companyStartY, { align: "right" }); doc.setFont("helvetica", "normal"); doc.setTextColor(...COLORS.muted); doc.text(COMPANY.address, rightEdge, companyStartY + lineH, { align: "right" }); doc.text(COMPANY.postalCity, rightEdge, companyStartY + lineH * 2, { align: "right" }); doc.setFontSize(7); doc.text(`VAT: ${COMPANY.vat}`, rightEdge, companyStartY + lineH * 3, { align: "right" }); // Logo (top-left, sized to match the 4 rows of company info) const logoH = lineH * 4; const logoW = logoH * 2.9; const logoY = companyStartY - 1; try { doc.addImage(EACH_LOGO_BASE64, "PNG", margin, logoY, logoW, logoH); } catch { doc.setFontSize(16); doc.setFont("helvetica", "bold"); doc.setTextColor(...COLORS.primary); doc.text("EACH", margin, companyStartY + 5); doc.setFontSize(7); doc.setFont("helvetica", "normal"); doc.text("CONNECTING SOLUTIONS", margin, companyStartY + 10); } // ============================================= // THIN SEPARATOR // ============================================= const sepY = logoY + logoH + 4; doc.setDrawColor(...COLORS.border); doc.setLineWidth(0.25); doc.line(margin, sepY, rightEdge, sepY); // ============================================= // DOCUMENT TITLE -- large, prominent label // ============================================= const titleY = sepY + 9; doc.setFontSize(18); doc.setFont("helvetica", "bold"); doc.setTextColor(...COLORS.accent); doc.text(options.title, pageWidth / 2, titleY, { align: "center" }); // Accent bar below title const underY = titleY + 3; const titleWidth = doc.getTextWidth(options.title); const underX = (pageWidth - titleWidth) / 2 - 6; doc.setDrawColor(...COLORS.accent); doc.setLineWidth(0.8); doc.line(underX, underY, underX + titleWidth + 12, underY); // ============================================= // DOCUMENT NUMBER + DATE // ============================================= const infoY = underY + 8; doc.setFontSize(8); doc.setFont("helvetica", "normal"); doc.setTextColor(...COLORS.muted); doc.text("No.", margin, infoY); doc.setFont("helvetica", "bold"); doc.setTextColor(...COLORS.primary); doc.text(options.documentNumber, margin + 8, infoY); doc.setFont("helvetica", "normal"); doc.setTextColor(...COLORS.muted); doc.text("Date:", rightEdge - 24, infoY); doc.setFont("helvetica", "bold"); doc.setTextColor(...COLORS.primary); doc.text(options.date, rightEdge, infoY, { align: "right" }); // ============================================= // RECIPIENT BOX // ============================================= const boxY = infoY + 5; const { recipient } = options; // Calculate dynamic box height based on content let boxContentH = 18; // minimum if (recipient.address) boxContentH += 4; if (recipient.phone) boxContentH += 3.5; if (recipient.email) boxContentH += 3.5; if (recipient.code) boxContentH += 3.5; doc.setFillColor(...COLORS.bgLight); doc.setDrawColor(...COLORS.border); doc.setLineWidth(0.25); doc.roundedRect(margin, boxY, contentWidth, boxContentH, 1.5, 1.5, "FD"); // Left column -- recipient identity const innerPad = 5; let ly = boxY + 5; doc.setFontSize(6.5); doc.setFont("helvetica", "bold"); doc.setTextColor(...COLORS.accent); doc.text(recipient.label, margin + innerPad, ly); ly += 4; doc.setFontSize(10); doc.setFont("helvetica", "bold"); doc.setTextColor(...COLORS.primary); doc.text(recipient.name, margin + innerPad, ly); ly += 4.5; doc.setFontSize(7.5); doc.setFont("helvetica", "normal"); doc.setTextColor(...COLORS.secondary); if (recipient.code) { doc.text(`Code: ${recipient.code}`, margin + innerPad, ly); ly += 3.5; } // Right column -- contact details const rightColX = margin + contentWidth / 2 + innerPad; let ry = boxY + 5; doc.setFontSize(6.5); doc.setFont("helvetica", "bold"); doc.setTextColor(...COLORS.accent); doc.text("CONTACT", rightColX, ry); ry += 4; doc.setFontSize(7.5); doc.setFont("helvetica", "normal"); doc.setTextColor(...COLORS.primary); if (recipient.contact) { doc.setFont("helvetica", "bold"); doc.text(recipient.contact, rightColX, ry); doc.setFont("helvetica", "normal"); ry += 3.5; } doc.setTextColor(...COLORS.secondary); if (recipient.address) { const addrLines = doc.splitTextToSize(recipient.address, contentWidth / 2 - innerPad * 2); doc.text(addrLines, rightColX, ry); ry += addrLines.length * 3.5; } if (recipient.phone) { doc.text(`Tel: ${recipient.phone}`, rightColX, ry); ry += 3.5; } if (recipient.email) { doc.text(recipient.email, rightColX, ry); ry += 3.5; } const contentY = boxY + boxContentH + 6; return { doc, contentY }; } /** * Adds page numbers as a minimal footer to all pages. */ export function addBrandedFooter(doc: jsPDF) { const pageCount = doc.getNumberOfPages(); const pageWidth = doc.internal.pageSize.getWidth(); const pageHeight = doc.internal.pageSize.getHeight(); const margin = 14; for (let i = 1; i <= pageCount; i++) { doc.setPage(i); doc.setDrawColor(...COLORS.border); doc.setLineWidth(0.2); doc.line(margin, pageHeight - 12, pageWidth - margin, pageHeight - 12); doc.setFontSize(7); doc.setFont("helvetica", "normal"); doc.setTextColor(...COLORS.muted); doc.text( `Page ${i} of ${pageCount}`, pageWidth / 2, pageHeight - 7, { align: "center" } ); } } /** * Shared table head styles for a unified look across all documents. */ export const TABLE_HEAD_STYLES = { fillColor: [247, 247, 247] as [number, number, number], textColor: [23, 23, 23] as [number, number, number], fontSize: 7.5, fontStyle: "bold" as const, cellPadding: 2.5, }; /** * Shared table body styles. */ export const TABLE_BODY_STYLES = { fontSize: 7.5, cellPadding: 2, textColor: [64, 64, 64] as [number, number, number], }; /** * Shared table border/grid styles. */ export const TABLE_STYLES = { lineColor: [210, 210, 210] as [number, number, number], lineWidth: 0.2, };