Files
website-secure/lib/pdf-template.ts
T
pm 9c14a19a87
DevSecOps Enterprise Pipeline / security-gate-and-deploy (push) Failing after 41s
chore: upload parte 1 - logica
2026-06-01 15:35:53 +01:00

264 lines
12 KiB
TypeScript

import jsPDF from "jspdf";
// Cropped & compressed logo (3KB, whitespace trimmed)
export const EACH_LOGO_BASE64 = "data:image/png;base64,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";
// 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,
};