# Sprint 1.5 — Etapa 5: PDF — Layout híbrido portrait + landscape > **Objetivo:** cambiar la orientación de la página que contiene el diagrama BPMN (heatmap) a landscape, mientras el resto del PDF se mantiene en portrait. Aplica a los 3 PDFs (Actual, Automatizado, ROI). > > **Pre-requisitos:** Etapa 4 cerrada — header y footer compartidos, paleta InQ aplicada en headers/footers. > > **Etapa siguiente:** Etapa 6 — Rediseño completo de página 1 del PDF ROI (KPI Hero gradiente, top 3, header denso). --- ## 1. Por qué este cambio El diagrama BPMN con heatmap es el elemento visual más importante del PDF. En layout vertical A4 (210mm de ancho útil), procesos con más de 7-8 actividades no caben legiblemente — el diagrama se renderiza chiquito y los nombres de las actividades se truncan. En landscape A4 (297mm de ancho útil), el diagrama tiene 41% más espacio horizontal. Los procesos típicos de InQuality (10-15 actividades con gateways) se ven legibles. **Es la diferencia entre un PDF que se entiende y uno que se descarga y nadie lee.** --- ## 2. Alcance estricto de Etapa 5 ### Sí entra - ✅ La página del diagrama BPMN (heatmap) cambia de portrait a landscape en los 3 PDFs (Actual, Automatizado, ROI) - ✅ Las demás páginas (resumen ejecutivo, tabla comparativa, metodología) se mantienen en portrait - ✅ El header y footer compartidos (creados en Etapa 4) se adaptan al ancho de la página landscape - ✅ El diagrama BPMN renderizado ocupa MÁS espacio horizontal (no queda con padding excesivo) - ✅ Tests E2E verifican que cada PDF tiene la mezcla correcta de orientaciones ### NO entra - ❌ Rediseño del contenido del diagrama (Etapa 6 o posterior) - ❌ Cambio de la leyenda del heatmap (bajo costo / alto costo) — solo se adapta al ancho landscape - ❌ Cambio de los KPI cards de la página 1 (Etapa 6) - ❌ Cambio de la tabla comparativa (Etapa 7) - ❌ Embebido de fonts Unicode (Sprint 2, anotado en TODO) - ❌ Tocar el resto de páginas del PDF --- ## 3. Decisiones técnicas obligatorias ### 3.1 Método correcto para cambiar orientación en jsPDF **SOLO usar este método** — los otros tienen bugs conocidos: ```typescript // ✅ CORRECTO — usar el parámetro de orientación en addPage doc.addPage('a4', 'landscape'); // Para volver a portrait después: doc.addPage('a4', 'portrait'); ``` **Métodos prohibidos:** ```typescript // ❌ NO USAR — método deprecated y comportamiento inconsistente doc.addPage([842, 595]); // dimensiones manuales sin orientación // ❌ NO USAR — rota el ordering de páginas y autotable doc.internal.pageSize.width = 842; doc.internal.pageSize.height = 595; // ❌ NO USAR — transform/rotate de contenido sobre página portrait // "Rotar el contenido 90° en una página portrait" se ve mal y rompe // el cálculo de posiciones de texto ``` ### 3.2 Detección del ancho disponible en cada página El módulo `pdf-sections-shared.ts` ya recibe el `doc` y debería usar: ```typescript const pageWidth = doc.internal.pageSize.getWidth(); const pageHeight = doc.internal.pageSize.getHeight(); ``` Esto retorna dinámicamente el ancho de la página ACTUAL (portrait o landscape). **NO hardcodear `595` (A4 portrait) ni `842` (A4 landscape).** Si encontrás esos números mágicos en el código, reemplazalos. ### 3.3 Generación del diagrama BPMN en landscape El diagrama BPMN se renderiza con html2canvas → imagen JPEG embebida en el PDF. La función actual probablemente está calculando dimensiones basadas en página portrait. **Hay que ajustarla para landscape:** ```typescript // Antes (asumiendo portrait): const maxImageWidth = pageWidth - 2 * marginX; // ~170mm const imageHeight = maxImageWidth * (canvas.height / canvas.width); // Después (funciona en ambas orientaciones): const pageWidth = doc.internal.pageSize.getWidth(); // dinámico const pageHeight = doc.internal.pageSize.getHeight(); // dinámico const usableWidth = pageWidth - 2 * marginX; const usableHeight = pageHeight - headerHeight - footerHeight - 2 * marginY; // Calcular escala que cabe en ambas dimensiones const scaleX = usableWidth / canvas.width; const scaleY = usableHeight / canvas.height; const scale = Math.min(scaleX, scaleY); const imageWidth = canvas.width * scale; const imageHeight = canvas.height * scale; ``` Esto garantiza que el diagrama: - Aprovecha el espacio horizontal en landscape - No se sale de los márgenes - Mantiene aspect ratio del canvas original ### 3.4 Páginas con qué orientación — orden y dimensiones **PDF Actual y Automatizado (3 páginas):** | Página | Contenido | Orientación | |---|---|---| | 1 | Resumen ejecutivo (KPI cards) | Portrait | | 2 | Diagrama BPMN con heatmap | **Landscape** | | 3 | Tabla de costos + nota metodológica | Portrait | **PDF ROI (4 páginas):** | Página | Contenido | Orientación | |---|---|---| | 1 | Resumen ejecutivo ROI (KPI hero + cards) | Portrait | | 2 | Comparación por actividad (tabla) | Portrait | | 3 | Composición + trayectoria + transparencia | Portrait | | 4 | Nota metodológica | Portrait | **Decisión arquitectónica:** el PDF ROI **NO incluye página con diagrama BPMN** en este sprint. El BPMN está en los PDFs Actual y Automatizado. Si en el futuro se decide incluir BPMN en ROI, se replica el patrón landscape. ### 3.5 Header y footer en página landscape El header y footer compartidos ya están en `pdf-sections-shared.ts` (Etapa 4). Deben: - Adaptarse al nuevo ancho automáticamente (porque usan `getWidth()`, no constantes) - Mantener la misma estructura visual (logo izquierda, metadata derecha, border-bottom naranja) - **NO requieren código nuevo si la implementación de Etapa 4 fue correcta** Si necesitás modificar `drawSharedHeader` o `drawSharedFooter` para soportar landscape, **PARÁ y consultá**. Probablemente significa que la implementación de Etapa 4 tiene un hardcode que se nos pasó. --- ## 4. Validaciones automatizadas mandatorias Antes de `present_files`, ejecutá y confirmá: ``` Para cada PDF generado: 1. Cantidad de páginas correcta: - actual: 3 páginas - automatizado: 3 páginas - roi: 4 páginas 2. Orientación de cada página (usando pdf-parse o pdfjs-dist para detectar): - actual: portrait, LANDSCAPE, portrait - automatizado: portrait, LANDSCAPE, portrait - roi: portrait, portrait, portrait, portrait 3. Header y footer presentes en TODAS las páginas (incluyendo landscape): - "InQ ROI" en header de cada página - "Powered by InQuality" en footer de cada página - Paginación correcta "Página X de Y" 4. Análisis de píxeles del diagrama BPMN en página 2 (actual/automatizado): - El ancho del diagrama supera 200mm (40% más que en portrait) - Heatmap presente con verde + ámbar + rojo - Texto de las actividades legible (no truncado a "...") 5. Tests previos verdes: >= 469 6. NO hay regresiones en marcas: - "InQ ROI" presente en todas las páginas - "Process Cost Platform" ausente - "v0.1.0" ausente ``` Si CUALQUIER validación falla, NO uses `present_files`. Reportá qué falló y esperá instrucciones. --- ## 5. Entregables (Definition of Done) ``` [ ] Página BPMN del PDF Actual ahora en landscape [ ] Página BPMN del PDF Automatizado ahora en landscape [ ] PDF ROI mantiene las 4 páginas en portrait (no tiene página BPMN) [ ] Header y footer compartidos funcionan correctamente en ambas orientaciones SIN modificar pdf-sections-shared.ts (si requirió modificación, justificá la causa) [ ] Diagrama BPMN aprovecha el ancho landscape (mínimo 200mm de ancho) [ ] 6 validaciones automatizadas confirmadas [ ] PDFs presentados con present_files [ ] Tests previos siguen verdes (469+) [ ] Tests nuevos para verificar orientación de páginas (3-5 casos mínimo) [ ] npm run build limpio [ ] Decisiones tomadas por iniciativa documentadas en reporte (Patrón C.6) ``` --- ## 6. Tests obligatorios ### 6.1 Tests unitarios Crear `tests/lib/export/pdf-orientation.test.ts`: ```typescript describe('PDF orientation', () => { it('el PDF actual tiene mezcla portrait-landscape-portrait', async () => { const pdf = await generateActualPdf(mockData); const orientations = getPageOrientations(pdf); expect(orientations).toEqual(['portrait', 'landscape', 'portrait']); }); it('el PDF automatizado tiene mezcla portrait-landscape-portrait', async () => { const pdf = await generateAutomatedPdf(mockData); const orientations = getPageOrientations(pdf); expect(orientations).toEqual(['portrait', 'landscape', 'portrait']); }); it('el PDF ROI mantiene las 4 páginas en portrait', async () => { const pdf = await generateRoiPdf(mockData); const orientations = getPageOrientations(pdf); expect(orientations).toEqual(['portrait', 'portrait', 'portrait', 'portrait']); }); it('el diagrama BPMN en landscape ocupa más de 200mm de ancho', async () => { const pdf = await generateActualPdf(mockData); const bpmnImageWidth = getImageWidthOnPage(pdf, 2); expect(bpmnImageWidth).toBeGreaterThan(200); }); it('header funciona en página landscape sin modificación', async () => { // verificar que drawSharedHeader se llama desde página landscape // y produce output coherente (logo izquierda, fecha derecha) }); }); ``` --- ## 7. Reporte final a entregar Cuando termines, devolveme: 1. **Tabla de archivos modificados/creados** con líneas 2. **Resultado de las 6 validaciones automatizadas** 3. **Cantidad final de tests verdes** (esperado 474+) 4. **3 PDFs presentados con `present_files`** para validación visual 5. **Comparación de ancho del diagrama**: ¿antes (portrait) vs ahora (landscape)? 6. **Hallazgos no esperados** 7. **Decisiones tomadas por iniciativa** según política CLAUDE.md NO arranques Etapa 6 sin mi OK explícito. --- ## 8. Reglas operativas (recordatorios) 1. **Usar SOLO `doc.addPage('a4', 'landscape')`**. Los otros métodos están prohibidos por gotchas conocidos. 2. **No hardcodear dimensiones de página**. Usar `doc.internal.pageSize.getWidth()` y `getHeight()` siempre. 3. **No modificar pdf-sections-shared.ts** salvo causa documentada. Si la implementación de Etapa 4 fue correcta, el header/footer deben funcionar en landscape sin tocar. 4. **No tocar contenido de páginas que NO son la BPMN**. El resto del PDF se mantiene visualmente igual. 5. **No racionalizar problemas de renderizado**. Si el diagrama se ve mal en landscape, investigar causa raíz (probablemente cálculo de escala). 6. **Política de iniciativa proactiva (CLAUDE.md)**: - Cambios Nivel 1 (refactor interno, mejor naming): ejecutar - Cambios Nivel 2 (visible pero reversible): ejecutar y reportar - Cambios Nivel 3 (marca, arquitectura, datos): CONSULTAR antes 7. **Si encontrás contradicción** entre este prompt y la realidad del código: **PARÁ y consultá**. --- ## 9. Riesgo conocido — orientation switching en jsPDF Tres bugs históricos en jsPDF al cambiar orientación entre páginas: 1. **Bug del autotable que se reinicia mal**: si la página landscape contiene una tabla autotable y la siguiente página vuelve a portrait, autotable puede continuar en el ancho de la landscape. **Solución**: la página landscape de esta etapa contiene solo el diagrama BPMN como imagen, no tablas. Si en el futuro hay una tabla en landscape, requiere atención adicional. 2. **Bug de imagen recortada al cambiar orientación**: si después de `addPage('a4', 'landscape')` no se recalcula el ancho disponible, la imagen se inserta con el ancho anterior (portrait) y se ve chiquita o se corta. **Solución**: siempre llamar `getWidth()` después de `addPage`. 3. **Footer absoluto en posición incorrecta**: si el footer se posiciona con `doc.internal.pageSize.height - 20`, en landscape eso es ~575px, pero la altura landscape A4 es ~595px. **Solución**: usar `getHeight()` dinámico, no asumir altura. Si durante la implementación encontrás algún comportamiento extraño en jsPDF que no esté en esta lista, **investigá causa raíz antes de aplicar workaround**. Documentá el hallazgo en el reporte final.