- Header/footer compartidos con paleta InQ naranja en los 3 PDFs (Etapa 4) - Layout híbrido portrait+landscape en PDFs Actual/Automatizado: página 2 landscape para diagrama BPMN heatmap (Etapa 5) - Página 1 PDF ROI rediseñada: header denso, banner narrativo, KPI Hero con gradiente naranja→magenta, grid 2×2 de KPIs secundarios, Top 3 actividades por ahorro (Etapa 6) - Páginas 2-5 PDF ROI con identidad InQ: tabla con paleta naranja, análisis del ahorro, nota metodológica, trayectoria del ahorro acumulado con gráfico SVG nativo (Etapa 7) - 500 tests verdes (+ 34 tests nuevos del Sprint 1.5) - BACKLOG.md, TECH_DEBT.md, OBSERVATIONS.md: sistema de planificación reemplaza TODO.md Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
280 lines
12 KiB
Markdown
280 lines
12 KiB
Markdown
# 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. |