loadImageBase64 ahora retorna { base64, pixelWidth, pixelHeight } (320×210px tras el resize) en vez de solo el string.
PdfHeaderOptions agrega logoPixelWidth/logoPixelHeight.
drawSharedHeader: logo a 15mm de ancho con alto calculado proporcionalmente desde las dimensiones reales (≈9.8mm, ratio real ~1.53:1, no el 3.56:1 hardcodeado anterior). El logo se posiciona arriba (MARGIN - 1), y processName + fecha bajan a la línea siguiente sin superponerse. clientName baja una línea adicional cuando hay logo (para no chocar con la fecha), ajustando y antes del border-bottom.
Fallback a texto "InQ ROI" intacto cuando no hay logo.
5.8 KiB
Etapa 1 — Logo InQuality en header de PDFs + whitespace-nowrap
Sprint: 6 Fecha: 2026-06-23 Alcance: 3 PDFs (Actual, Automatizado, ROI) + un fix de una línea en ComparisonTable.
Contexto
El header compartido de los 3 PDFs (src/lib/export/pdf-sections-shared.ts, función drawSharedHeader) hoy dibuja el texto "InQ ROI" en naranja bold como identidad de marca. El logo PNG de InQuality ya existe en assets/. El objetivo es reemplazar el texto por el logo, manteniendo el layout del header sin cambios estructurales.
Parte A — Logo en header de PDFs
Assets disponibles
En assets/ (ruta desde la raíz del proyecto):
MARCA FINAL 2023 AREA DE SEGURIDAD 1 COLOR.png— logo horizontal color, sobre fondos claros — usar esteMARCA FINAL 2023 AREA DE SEGURIDAD 1 BCO.png— logo horizontal blanco (no aplica, el header es blanco)
Cómo cargar el PNG en jsPDF
Importar la URL del asset con Vite:
// En pdf-export.ts (o en un helper de carga)
import logoColorUrl from '../../../assets/MARCA FINAL 2023 AREA DE SEGURIDAD 1 COLOR.png?url'
Convertir a base64 antes de generar cualquier PDF (operación async, una vez por export):
async function loadImageBase64(url: string): Promise<string> {
const response = await fetch(url)
const blob = await response.blob()
return new Promise((resolve, reject) => {
const reader = new FileReader()
reader.onload = () => resolve(reader.result as string)
reader.onerror = reject
reader.readAsDataURL(blob)
})
}
Llamar una vez al inicio de cada función de export (exportPdf, exportRoiPdf) y pasar el resultado como parámetro.
Cambios en PdfHeaderOptions
Agregar un campo opcional:
export interface PdfHeaderOptions {
pageNumber: number
processName: string
sectionTitle?: string
clientName?: string
simulatedAt: number
logoBase64?: string // ← nuevo, opcional para retrocompatibilidad
}
Cambios en drawSharedHeader
Reemplazar el bloque de texto "InQ ROI" por lógica condicional:
// ANTES:
doc.setFont('helvetica', 'bold')
doc.setFontSize(13)
doc.setTextColor(...PDF_COLORS.inqOrange)
doc.text('InQ ROI', MARGIN, y)
// DESPUÉS:
if (options.logoBase64) {
// Logo PNG: ancho 32mm, alto proporcional (~9mm para este logo horizontal)
// Alinear el baseline del logo con la primera línea de texto: y - 7 (arriba del baseline)
doc.addImage(options.logoBase64, 'PNG', MARGIN, y - 7, 32, 9)
} else {
// Fallback al texto (retrocompatibilidad y tests)
doc.setFont('helvetica', 'bold')
doc.setFontSize(13)
doc.setTextColor(...PDF_COLORS.inqOrange)
doc.text('InQ ROI', MARGIN, y)
}
Nota sobre dimensiones: el logo horizontal de InQuality tiene una proporción aproximada de 3.5:1 (ancho:alto). A 32mm de ancho → ~9mm de alto. Ajustar si el resultado visual no queda bien — la validación visual del director confirma antes del OK formal.
Cambios en pdf-export.ts
En las dos funciones de export (exportPdf y exportRoiPdf), agregar la carga del logo al inicio:
export async function exportPdf(...): Promise<void> {
const logoBase64 = await loadImageBase64(logoColorUrl).catch(() => undefined)
const headerOpts: PdfHeaderOptions = {
// ...opciones existentes...
logoBase64,
}
// resto sin cambios
}
El .catch(() => undefined) garantiza que si el fetch del asset falla (ej: entorno sin red), el PDF se genera igual con el fallback de texto. No bloquear el export por el logo.
Importante — NO cambiar
- La estructura del header (posiciones de proceso, cliente, fecha, border-bottom) no cambia.
- El footer ("Powered by InQuality") no cambia.
drawSharedFooteryapplyFootersToAllPagesno se tocan.- No agregar el logo en páginas 1 de cada PDF que tengan su propio diseño de portada (verificar si
drawScenarioPage1ydrawRoiPage1usan o nodrawSharedHeader— si no lo usan, el logo solo aplica a las páginas que sí lo llaman).
Parte B — whitespace-nowrap en celda de Eficiencia
En src/features/report/ComparisonTable.tsx, dentro de MetricCell, el <span> que muestra el valor numérico de la columna "Eficiencia" se corta en dos líneas ("1.02" / "USD/min") porque la celda es angosta.
Fix: agregar whitespace-nowrap al className del <span>:
// Buscar el span que tiene el valor formateado de la Eficiencia y agregar whitespace-nowrap
<span className="font-mono text-slate-700 text-xs w-14 text-right whitespace-nowrap">
{format(value)}
</span>
Una línea de cambio. Sin tests que actualizar.
Criterios de validación
- Logo visible en el header de todas las páginas de los 3 PDFs (Actual, Automatizado, ROI)
- Logo en tamaño coherente con el header — no dominante, no microscópico
- Alineación correcta: logo a la izquierda, fecha y cliente a la derecha
- El footer "Powered by InQuality" sigue presente en todas las páginas
- Si el asset no carga (fetch falla), el PDF se genera igual con texto "InQ ROI" como fallback
- "1.02 USD/min" aparece en una sola línea en ComparisonTable
npm run lintsin erroresnpm run buildlimpionpm run test→ 542 tests verdes (sin regresiones — los tests de PDF existentes usan mocks y no verifican el logo)- Validación visual del director sobre el PDF exportado antes del OK formal — esta es la validación principal de la etapa
Archivos a modificar
src/lib/export/pdf-sections-shared.ts—PdfHeaderOptions+drawSharedHeadersrc/lib/export/pdf-export.ts— carga del logo + pasarlogoBase64aheaderOptssrc/features/report/ComparisonTable.tsx—whitespace-nowrapenMetricCell
No modificar ningún otro archivo. En particular, no tocar los tests de PDF existentes — el fallback de texto garantiza que sigan pasando sin cambios.