# Sprint 2 — Etapa 5: PDF — Tabla de recursos por actividad + verificación de truncamiento

> **Tipo:** export PDF. Sin cambios en dominio, store, persistencia ni componentes React.
> **Objetivo:** agregar una sección condicional al PDF de escenario (actual/automatizado) con la tabla de recursos por actividad. Cerrar el ítem de truncamiento de TECH_DEBT.
> **Resultado esperado:** PDF con nueva página de recursos (solo cuando hay datos), texto sin truncamiento en notas metodológicas. Validación visual del director requerida antes del OK formal.

---

## Archivos a modificar

```
src/lib/export/pdf-sections.ts     ← nueva función drawResourcesSection + fix truncamiento
src/lib/export/pdf-export.ts       ← agregar activities al llamado de exportScenarioPdf
```

Si durante la implementación identificás que `src/lib/export/pdf-sections-roi.ts` también
tiene líneas truncadas, podés incluirlo. Si aparece cualquier otro archivo no listado,
**detenete y reportá el conflicto antes de tocarlo**.

---

## Feature 1 — Tabla de recursos por actividad en PDF escenario

### Contexto: dónde vive el dato

`result.perActivity[].resourceCostBreakdown: Array<{ resourceId, resourceName, cost }>`
— ya está en el resultado de simulación. El campo `cost` es el costo esperado
(ponderado por probabilidad de ejecución).

Para mostrar `costPerHour`, `utilizationMinutes` y `units` (columnas que el cliente
puede verificar), necesitamos cruzar con `Activity.assignedResources`. Eso requiere
pasar `activities` a `exportScenarioPdf`.

### Cambio 1 — pdf-export.ts: pasar `activities` a `exportScenarioPdf`

```typescript
// Firma actual (línea ~54)
async function exportScenarioPdf(
  doc: any,
  autoTable: Function,
  process: Process,
  simulation: Simulation,
  result: Simulation['result'],
  resources: Resource[],
  heatmapImageData: string | null,
  currency: string,
  tab: 'actual' | 'automatizado'
): Promise<void>

// Firma nueva — agregar activities al final
async function exportScenarioPdf(
  doc: any,
  autoTable: Function,
  process: Process,
  simulation: Simulation,
  result: Simulation['result'],
  resources: Resource[],
  heatmapImageData: string | null,
  currency: string,
  tab: 'actual' | 'automatizado',
  activities: Activity[]             // ← nuevo parámetro, default []
): Promise<void>
```

En la llamada desde `exportToPdf` (línea ~44), agregar `activities` al final:

```typescript
await exportScenarioPdf(doc, autoTable, process, simulation, result, resources, heatmapImageData, currency, tab as 'actual' | 'automatizado', activities)
```

`activities` ya está destructurado en `exportToPdf` — no hay que cambiarlo ahí.

### Cambio 2 — pdf-export.ts: insertar página condicional de recursos

En `exportScenarioPdf`, reemplazar el bloque de página 4 (metodología) actual por:

```typescript
// Evaluar si hay recursos para mostrar
const hasResources = result.perActivity.some(act => act.resourceCostBreakdown.length > 0)

// ── Página 4 (condicional): Tabla de recursos ────────────────────────────────
if (hasResources) {
  docAny.addPage('a4', 'portrait')
  y = drawSharedHeader(docAny, { ...headerOpts, pageNumber: 4 })
  drawResourcesSection(docAny, autoTable, result, resources, activities, currency, y)
}

// ── Página 4 o 5: Nota metodológica ─────────────────────────────────────────
docAny.addPage('a4', 'portrait')
y = drawSharedHeader(docAny, { ...headerOpts, pageNumber: hasResources ? 5 : 4 })
drawMethodologySection(docAny, process, y)
```

### Cambio 3 — pdf-sections.ts: nueva función `drawResourcesSection`

Agregar esta función exportada al final de `pdf-sections.ts`, antes del bloque de
funciones privadas. Importar los tipos necesarios si no están importados ya
(`ActivitySimResult`, `Resource`, `Activity`).

```typescript
export function drawResourcesSection(
  doc: any,
  autoTable: Function,
  result: { perActivity: ActivitySimResult[] },
  resources: Resource[],
  activities: Activity[],
  currency: string,
  startY: number
): void {
  const m = PDF.margin
  const cW = PDF.contentW

  // Título de sección
  doc.setFont('helvetica', 'bold')
  doc.setFontSize(10)
  doc.setTextColor(...PDF.slate900)
  doc.text('DESGLOSE DE RECURSOS POR ACTIVIDAD', m, startY)
  startY += 2
  doc.setDrawColor(...PDF.slate200)
  doc.line(m, startY, m + cW, startY)
  startY += 5

  doc.setFont('helvetica', 'normal')
  doc.setFontSize(8)
  doc.setTextColor(...PDF.slate500)
  doc.text('Costo por ejecución, ponderado por probabilidad de ejecución de cada actividad.', m, startY)
  startY += 6

  // Mapa rápido: resourceId → Resource para lookup de tipo y costo/hora
  const resourceMap = new Map(resources.map(r => [r.id, r]))

  // Mapa: activityId → Activity para lookup de assignedResources
  const activityMap = new Map(activities.map(a => [a.id, a]))

  // Tipos de recurso en español y colores de celda (RGB para jsPDF)
  const TYPE_LABEL: Record<string, string> = {
    role: 'Rol',
    person: 'Persona',
    system: 'Sistema',
    equipment: 'Equipamiento',
    supply: 'Insumo',
  }
  const TYPE_COLOR: Record<string, [number, number, number]> = {
    role:      [255, 237, 213],  // naranja claro
    person:    [220, 252, 231],  // verde claro
    system:    [219, 234, 254],  // azul claro
    equipment: [241, 245, 249],  // slate claro
    supply:    [254, 249, 195],  // amarillo claro
  }

  const RA = 'right' as const

  // Construir filas agrupadas por actividad
  const tableHead = [[
    { content: 'Actividad', styles: { cellWidth: 40 } },
    { content: 'Recurso', styles: { cellWidth: 38 } },
    { content: 'Tipo', styles: { cellWidth: 20 } },
    { content: `$/h (${currency})`, styles: { halign: RA, cellWidth: 20 } },
    { content: 'Min.', styles: { halign: RA, cellWidth: 14 } },
    { content: 'Uds.', styles: { halign: RA, cellWidth: 12 } },
    { content: `Costo esp. (${currency})`, styles: { halign: RA, cellWidth: 26 } },
  ]]

  const tableBody: any[] = []
  const cellStyles: Record<string, any> = {}
  let rowIndex = 0

  for (const act of result.perActivity) {
    if (act.resourceCostBreakdown.length === 0) continue

    const activityRow = activityMap.get(act.activityId)
    const firstRow = true

    for (let i = 0; i < act.resourceCostBreakdown.length; i++) {
      const br = act.resourceCostBreakdown[i]
      const resource = resourceMap.get(br.resourceId)
      const assignment = activityRow?.assignedResources.find(a => a.resourceId === br.resourceId)

      const typeName = resource ? (TYPE_LABEL[resource.type] ?? resource.type) : '—'
      const typeColor = resource ? (TYPE_COLOR[resource.type] ?? PDF.slate50) : PDF.slate50
      const costPerHour = resource ? formatNum(resource.costPerHour) : '—'
      const minutes = assignment ? String(assignment.utilizationMinutes) : '—'
      const units = assignment ? String(assignment.units) : '—'

      tableBody.push([
        i === 0 ? { content: act.activityName, rowSpan: act.resourceCostBreakdown.length } : null,
        { content: br.resourceName },
        { content: typeName, styles: { fillColor: typeColor, fontStyle: 'bold', fontSize: 7 } },
        { content: costPerHour, styles: { halign: RA, font: 'courier' } },
        { content: minutes, styles: { halign: RA, font: 'courier' } },
        { content: units, styles: { halign: RA, font: 'courier' } },
        { content: formatNum(br.cost), styles: { halign: RA, font: 'courier' } },
      ].filter(Boolean))

      rowIndex++
    }

    // Fila de subtotal por actividad
    const subtotal = act.resourceCostBreakdown.reduce((s, r) => s + r.cost, 0)
    tableBody.push([
      { content: '', styles: { fillColor: [255, 248, 237] } },
      { content: 'Subtotal recursos', colSpan: 5, styles: { fontStyle: 'italic', fontSize: 7.5, fillColor: [255, 248, 237] } },
      { content: formatNum(subtotal), styles: { halign: RA, font: 'courier', fontStyle: 'bold', fillColor: [255, 248, 237] } },
    ])
    rowIndex++
  }

  if (tableBody.length === 0) return

  autoTable(doc, {
    head: tableHead,
    body: tableBody,
    startY,
    margin: { left: m, right: m },
    styles: { fontSize: 7.5, cellPadding: 2.5, overflow: 'ellipsize', lineColor: [226, 232, 240], lineWidth: 0.2 },
    headStyles: {
      fillColor: [255, 248, 237],
      textColor: [146, 64, 14],
      fontStyle: 'bold',
      fontSize: 8,
      lineColor: [245, 152, 69],
      lineWidth: 0.5,
    },
    alternateRowStyles: { fillColor: PDF.slate50 },
  })
}
```

**Nota sobre rowSpan:** jsPDF-autotable no soporta `rowSpan` en body rows de la misma
forma que en HTML. Si hay problemas con rowSpan, reemplazarlo por repetir el nombre de
la actividad en cada fila (sin rowSpan) — es menos elegante pero funciona igual. Reportar
qué solución se usó.

---

## Feature 2 — Verificación de truncamiento (TECH_DEBT)

### Contexto

El ítem en TECH_DEBT.md indica que las notas metodológicas podrían tener líneas largas.
Los caracteres rotos (Unicode) ya fueron resueltos en Sprint 1.5. El riesgo pendiente es
si alguna fórmula o línea larga supera el ancho de página portrait.

### Acción requerida

**Paso 1 — Búsqueda grep:**
```bash
grep -n "doc\.text(" src/lib/export/pdf-sections.ts src/lib/export/pdf-sections-roi.ts
```

Identificar todas las llamadas `doc.text(string, x, y)` donde `string` es una variable
de longitud potencialmente grande Y no está precedida por `splitTextToSize`.

**Paso 2 — Evaluación:**
- Para cada línea identificada, evaluar si el string puede superar `PDF.contentW` (170mm)
- Si es una cadena hardcodeada corta → sin riesgo, documentar y cerrar
- Si es variable (resourceName, processName, etc.) → aplicar `splitTextToSize`

**Paso 3 — Fix específico conocido:**

En `pdf-sections.ts`, función `drawAnalysisSection`, la línea:
```typescript
doc.text(`Recurso principal: "${topResource.resourceName}" — ${formatCurrency(topResource.totalCost, currency)}`, m, y)
```
…usa un `resourceName` de longitud variable sin wrap. Cambiar a:
```typescript
const resourceLine = doc.splitTextToSize(
  `Recurso principal: "${topResource.resourceName}" — ${formatCurrency(topResource.totalCost, currency)}`,
  cW
)
doc.text(resourceLine, m, y)
y += (resourceLine.length - 1) * 4.5
```

**Paso 4 — Reporte:** listar cada línea revisada, con resultado (sin riesgo / corregida).

---

## Verificaciones obligatorias antes de reportar

### 1. Verificación de scope con git diff — OBLIGATORIO
```bash
git diff --name-only HEAD
```

El resultado debe contener únicamente:
```
src/lib/export/pdf-sections.ts
src/lib/export/pdf-export.ts
src/lib/export/pdf-sections-roi.ts   ← solo si se detectó y corrigió truncamiento ahí
```

Si aparece cualquier archivo no listado → revertirlo, listarlo bajo "Conflictos de scope".

### 2. Build limpio
```bash
npm run build
```
Sin errores TypeScript.

### 3. Tests
```bash
npm run test
```
Todos los tests existentes deben seguir verdes. Esta etapa no agrega tests nuevos.

### 4. Smoke test visual

Exportar PDF de escenario actual con un proceso que tenga al menos una actividad con
recursos asignados. Verificar:

- [ ] El PDF incluye la nueva página de recursos antes de la metodología
- [ ] La tabla muestra correctamente: actividad, recurso, tipo, $/h, min, unidades, costo esperado
- [ ] La celda de "Tipo" tiene color de fondo según el tipo del recurso
- [ ] La fila de subtotal aparece al final de cada actividad con el total correcto
- [ ] El número de página en el header es correcto (4 si hay recursos, la metodología pasa a 5)
- [ ] Exportar un proceso SIN recursos: la página de recursos no aparece, metodología es página 4

---

## Alcance estricto — lo que NO entra en esta etapa

- ❌ PDF de ROI (`exportRoiPdf`) — no agregar tabla de recursos ahí. La tabla ROI ya tiene su propia lógica.
- ❌ Ningún componente React (`src/features/`)
- ❌ Dominio o store
- ❌ Cambios en el reporte web — Etapa 3 ya cerrada
- ❌ Nuevos tipos o schemas en `src/domain/`

---

## Reporte esperado al terminar

```
## Etapa 5 completada

### Archivos modificados
- src/lib/export/pdf-sections.ts — drawResourcesSection + fix truncamiento
- src/lib/export/pdf-export.ts — activities param + página condicional
- src/lib/export/pdf-sections-roi.ts — [sin cambios / fix truncamiento aplicado]

### Cambios realizados
- drawResourcesSection: [descripción breve del approach final — en especial si rowSpan se usó o se descartó]
- Página condicional: aparece si `result.perActivity.some(act => act.resourceCostBreakdown.length > 0)`
- Numeración de página: metodología pasa a pág. 5 cuando hay recursos, se mantiene en 4 si no
- Truncamiento: [lista de líneas auditadas y resultado de cada una]

### TECH_DEBT cerrados
- Truncamiento notas metodológicas: ✅ [auditado sin cambios / corregido en X líneas]

### Verificaciones
- git diff --name-only HEAD: [lista] ✅
- npm run build: ✅
- npm run test: X/X ✅
- Smoke test: [descripción breve]

### Conflictos de scope
[ninguno / lista con razones]

### Notas para Etapas siguientes
[hallazgos para Etapa 6 E2E]
```
