/** * Módulo compartido de header/footer PDF — Sprint 1.5 Etapa 4. * * Provee drawSharedHeader, drawSharedFooter, applyFootersToAllPages * y PDF_COLORS para uso en los 3 PDFs (Actual, Automatizado, ROI). * * Diseñado para ser compatible con páginas landscape (Etapa 5): * usa doc.internal.pageSize.getWidth/Height() en lugar de constantes A4. */ import { formatSimulationTimestamp } from '@/lib/format' // ─── Tipo DocLike ───────────────────────────────────────────────────────────── // Superset del DocLike anterior: agrega pageSize para compatibilidad landscape. // pdf-sections.ts re-exporta este tipo para mantener compatibilidad de imports. export type DocLike = { setFont: (name: string, style: string) => void setFontSize: (size: number) => void setTextColor: (...args: number[]) => void setFillColor: (...args: number[]) => void setDrawColor: (...args: number[]) => void setLineWidth: (w: number) => void text: (text: string | string[], x: number, y: number, opts?: Record) => void rect: (x: number, y: number, w: number, h: number, style?: string) => void roundedRect: (x: number, y: number, w: number, h: number, rx: number, ry: number, style?: string) => void line: (x1: number, y1: number, x2: number, y2: number) => void addImage: (img: string, format: string, x: number, y: number, w: number, h: number) => void addPage: () => void setPage: (page: number) => void internal: { getNumberOfPages: () => number pageSize: { getWidth: () => number getHeight: () => number } } splitTextToSize: (text: string, maxWidth: number) => string[] } // ─── Paleta PDF ─────────────────────────────────────────────────────────────── export const PDF_COLORS = { inqOrange: [245, 152, 69] as [number, number, number], // #F59845 inqOrangeDark: [180, 83, 9] as [number, number, number], // #B45309 inqMagenta: [237, 62, 143] as [number, number, number], // #ED3E8F textPrimary: [15, 23, 42] as [number, number, number], // #0F172A slate-900 textSecondary: [71, 85, 105] as [number, number, number], // #475569 slate-600 textTertiary: [100, 116, 139] as [number, number, number], // #64748B slate-500 textDisabled: [148, 163, 184] as [number, number, number], // #94A3B8 slate-400 borderLight: [226, 232, 240] as [number, number, number], // #E2E8F0 slate-200 slate50: [248, 250, 252] as [number, number, number], // #F8FAFC successGreen: [22, 163, 74] as [number, number, number], // #16A34A dangerRed: [239, 68, 68] as [number, number, number], // #EF4444 warningAmber: [245, 158, 11] as [number, number, number], // #F59E0B } as const const MARGIN = 20 // mm // ─── Header compartido ──────────────────────────────────────────────────────── export interface PdfHeaderOptions { pageNumber: number // 1-based — determina grosor del border (3px pág 1, 2px resto) processName: string sectionTitle?: string // subtítulo de sección, ej. "Análisis de retorno de inversión" clientName?: string simulatedAt: number // Unix timestamp ms } /** * Dibuja el header InQ-branded. * - Logo "InQ ROI" naranja a la izquierda, fecha a la derecha * - Nombre del proceso + cliente * - Border-bottom naranja (más grueso en página 1) * Retorna la coordenada y donde empieza el contenido. */ export function drawSharedHeader(doc: DocLike, options: PdfHeaderOptions): number { const pageW = doc.internal.pageSize.getWidth() const contentRight = pageW - MARGIN let y = MARGIN // Logo "InQ ROI" — naranja bold doc.setFont('helvetica', 'bold') doc.setFontSize(13) doc.setTextColor(...PDF_COLORS.inqOrange) doc.text('InQ ROI', MARGIN, y) // Fecha — derecha, slate-400 const { date } = formatSimulationTimestamp(options.simulatedAt) doc.setFont('helvetica', 'normal') doc.setFontSize(7.5) doc.setTextColor(...PDF_COLORS.textDisabled) doc.text(date, contentRight, y, { align: 'right' } as Record) y += 7 // Nombre del proceso — bold slate-900 doc.setFont('helvetica', 'bold') doc.setFontSize(11) doc.setTextColor(...PDF_COLORS.textPrimary) doc.text(options.processName, MARGIN, y) // Cliente — derecha, slate-500 if (options.clientName) { doc.setFont('helvetica', 'normal') doc.setFontSize(7.5) doc.setTextColor(...PDF_COLORS.textTertiary) doc.text(options.clientName, contentRight, y, { align: 'right' } as Record) } y += 5 // Subtítulo de sección (opcional) if (options.sectionTitle) { doc.setFont('helvetica', 'normal') doc.setFontSize(8.5) doc.setTextColor(...PDF_COLORS.textSecondary) doc.text(options.sectionTitle, MARGIN, y) y += 4.5 } // Border-bottom naranja InQ: 0.8mm (≈3pt) en página 1, 0.5mm (≈2pt) en el resto const borderWidth = options.pageNumber === 1 ? 0.8 : 0.5 doc.setDrawColor(...PDF_COLORS.inqOrange) doc.setLineWidth(borderWidth) doc.line(MARGIN, y, contentRight, y) y += 5 // Reset lineWidth para no afectar el contenido siguiente doc.setLineWidth(0.2) return y } // ─── Footer compartido ──────────────────────────────────────────────────────── export interface PdfFooterOptions { clientName?: string simulatedAt: number } /** * Dibuja el footer en la posición inferior fija de la página actual. * - Border-top slate-200 * - Izquierda: "InQ ROI · Powered by InQuality · [cliente] · [fecha]" * - Derecha: "Página X de Y" * No incluye versión del producto. */ export function drawSharedFooter( doc: DocLike, options: PdfFooterOptions & { pageNumber: number; totalPages: number } ): void { const pageW = doc.internal.pageSize.getWidth() const pageH = doc.internal.pageSize.getHeight() const contentRight = pageW - MARGIN const footerLineY = pageH - 14 const footerTextY = pageH - 9 // Border-top doc.setDrawColor(...PDF_COLORS.borderLight) doc.setLineWidth(0.2) doc.line(MARGIN, footerLineY, contentRight, footerLineY) // Texto izquierda const { date } = formatSimulationTimestamp(options.simulatedAt) const parts: string[] = ['InQ ROI', 'Powered by InQuality'] if (options.clientName) parts.push(options.clientName) parts.push(date) doc.setFont('helvetica', 'normal') doc.setFontSize(7) doc.setTextColor(...PDF_COLORS.textDisabled) doc.text(parts.join(' · '), MARGIN, footerTextY) // Paginación derecha doc.text( `Página ${options.pageNumber} de ${options.totalPages}`, contentRight, footerTextY, { align: 'right' } as Record ) } /** * Aplica footer a TODAS las páginas del documento. * Llamar después de agregar todo el contenido. */ export function applyFootersToAllPages(doc: DocLike, options: PdfFooterOptions): void { const total = doc.internal.getNumberOfPages() for (let i = 1; i <= total; i++) { doc.setPage(i) drawSharedFooter(doc, { ...options, pageNumber: i, totalPages: total }) } }