This commit is contained in:
@@ -24,7 +24,12 @@
|
|||||||
|
|
||||||
## Hallazgos resueltos durante el sprint
|
## Hallazgos resueltos durante el sprint
|
||||||
|
|
||||||
*(vacío)*
|
### [RESOLVED] Etapa 1 — Logo InQuality en header de PDFs (págs 2+)
|
||||||
|
**Observación 1:** Claude Code implementó inicialmente el logo con dimensiones hardcodeadas (32×9mm, ratio 3.56:1) cuando el PNG real es 724×474px (ratio 1.53:1) → logo aplastado horizontalmente. Fix: pasar dimensiones reales del bitmap y calcular el alto proporcional en `drawSharedHeader`.
|
||||||
|
**Observación 2:** Claude Code agregó "UN PRODUCTO DE InQ InQuality" y logo en la portada (página 1) sin que estuviera en el scope del prompt — violación de la restricción explícita "No agregar el logo en páginas 1 con su propio diseño de portada." Revertido en fix posterior.
|
||||||
|
**Aprendizaje del método:** cuando el prompt incluye restricciones del tipo "NO hacer X en archivo Y", el agente puede introducir el cambio equivalente en archivos relacionados no listados. Auditar activamente los archivos adyacentes al scope declarado.
|
||||||
|
**Resultado final:** logo isotipo a 15mm de ancho, alto proporcional (~9.8mm), páginas 2+ de los 3 PDFs. Portada intacta. whitespace-nowrap en MetricCell de Eficiencia incluido como fold-in.
|
||||||
|
**OK formal del director:** 2026-06-24.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
241
sprints/sprint-6/ETAPA_2_PROMPT.md
Normal file
241
sprints/sprint-6/ETAPA_2_PROMPT.md
Normal file
@@ -0,0 +1,241 @@
|
|||||||
|
# Etapa 2 — Panel de análisis de sensibilidad
|
||||||
|
|
||||||
|
**Sprint:** 6
|
||||||
|
**Fecha:** 2026-06-24
|
||||||
|
**Alcance:** nuevo componente `SensitivityPanel` + dos sliders + recálculo de KPIs en tiempo real + versión wow con doble línea en `CumulativeSavingsChart`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Contexto
|
||||||
|
|
||||||
|
El ROI que calcula InQ ROI hoy es nominal y puntual: dadas estas condiciones exactas, el retorno es X. Un CFO o interlocutor financiero va a preguntar en la reunión: "¿y si el volumen cae 20%?" o "¿y si la implementación sale más cara?". Sin poder responder eso en vivo, el informe pierde credibilidad.
|
||||||
|
|
||||||
|
El panel de sensibilidad responde esas preguntas sin re-simular. Los costos por ejecución ya están calculados y son estables. Solo cambian los agregados anuales. El recálculo es una llamada a `calculateRoi()` con inputs modificados — instantáneo.
|
||||||
|
|
||||||
|
**La restricción más importante:** NO re-simular. No tocar `simulation.ts`, no llamar al motor BPMN. Solo `domain/roi.ts`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Arquitectura — qué cambia y dónde
|
||||||
|
|
||||||
|
### Estado nuevo en `ReportPage.tsx`
|
||||||
|
|
||||||
|
Agregar al estado local:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
const [volumeAdjust, setVolumeAdjust] = useState(0) // entero: -50 a +100
|
||||||
|
const [investmentAdjust, setInvestmentAdjust] = useState(0) // entero: -50 a +100
|
||||||
|
```
|
||||||
|
|
||||||
|
Agregar un `useMemo` para el ROI ajustado, justo después del `roi` existente:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// ROI ajustado por sliders de sensibilidad — recalcula solo los agregados anuales,
|
||||||
|
// sin tocar costPerExecutionActual ni costPerExecutionAutomated (son estables).
|
||||||
|
const adjustedRoi = useMemo(() => {
|
||||||
|
if (!roi || !simulation?.result || !simulation?.resultAutomated || !process) return null
|
||||||
|
if (volumeAdjust === 0 && investmentAdjust === 0) return roi // sin cambios → mismo objeto
|
||||||
|
return calculateRoi({
|
||||||
|
costPerExecutionActual: simulation.result.totalCost,
|
||||||
|
costPerExecutionAutomated: simulation.resultAutomated.totalCost,
|
||||||
|
annualFrequency: process.annualFrequency * (1 + volumeAdjust / 100),
|
||||||
|
analysisHorizonYears: process.analysisHorizonYears,
|
||||||
|
automationInvestment: process.automationInvestment * (1 + investmentAdjust / 100),
|
||||||
|
})
|
||||||
|
}, [roi, simulation, process, volumeAdjust, investmentAdjust])
|
||||||
|
```
|
||||||
|
|
||||||
|
### Modificaciones en el tab "ROI" de `ReportPage.tsx`
|
||||||
|
|
||||||
|
**Orden del layout** (dentro del bloque `hasAutomatedResult && roi`):
|
||||||
|
|
||||||
|
1. `RoiKpiBar` — recibe `adjustedRoi ?? roi` en lugar de `roi`
|
||||||
|
2. `SensitivityPanel` — nuevo, debajo de los KPIs, colapsado por defecto
|
||||||
|
3. `ComparisonTable` — sin cambio (per-ejecución, no varía con los sliders)
|
||||||
|
4. Gráficos: `CumulativeSavingsChart` recibe `adjustedRoi` (y `roi` como base para la versión wow)
|
||||||
|
|
||||||
|
Cambios concretos en JSX:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
{/* KPIs de ROI — actualizados por sliders */}
|
||||||
|
<RoiKpiBar
|
||||||
|
roi={adjustedRoi ?? roi}
|
||||||
|
currency={process.currency}
|
||||||
|
horizonYears={process.analysisHorizonYears}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Panel de sensibilidad — colapsado por defecto */}
|
||||||
|
<SensitivityPanel
|
||||||
|
process={process}
|
||||||
|
volumeAdjust={volumeAdjust}
|
||||||
|
investmentAdjust={investmentAdjust}
|
||||||
|
onVolumeChange={setVolumeAdjust}
|
||||||
|
onInvestmentChange={setInvestmentAdjust}
|
||||||
|
onReset={() => { setVolumeAdjust(0); setInvestmentAdjust(0) }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Tabla de comparación — recibe roi base (per-ejecución, no varía) */}
|
||||||
|
<div className="mb-6">
|
||||||
|
<h2 ...>Comparación por actividad</h2>
|
||||||
|
<ComparisonTable ... />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Gráficos */}
|
||||||
|
<CumulativeSavingsChart
|
||||||
|
roi={adjustedRoi ?? roi}
|
||||||
|
roiBase={volumeAdjust !== 0 || investmentAdjust !== 0 ? roi : undefined}
|
||||||
|
horizonYears={process.analysisHorizonYears}
|
||||||
|
currency={process.currency}
|
||||||
|
investment={process.automationInvestment * (1 + investmentAdjust / 100)}
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Componente nuevo: `SensitivityPanel`
|
||||||
|
|
||||||
|
**Archivo:** `src/features/report/SensitivityPanel.tsx`
|
||||||
|
|
||||||
|
### Props
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
interface SensitivityPanelProps {
|
||||||
|
process: {
|
||||||
|
annualFrequency: number
|
||||||
|
automationInvestment: number
|
||||||
|
currency: string
|
||||||
|
}
|
||||||
|
volumeAdjust: number // entero -50 a +100
|
||||||
|
investmentAdjust: number // entero -50 a +100
|
||||||
|
onVolumeChange: (v: number) => void
|
||||||
|
onInvestmentChange: (v: number) => void
|
||||||
|
onReset: () => void
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Comportamiento
|
||||||
|
|
||||||
|
- Colapsado por defecto. El header siempre visible, con un chevron toggle.
|
||||||
|
- Cuando está expandido: dos sliders con label y valor actual.
|
||||||
|
- Cuando algún slider ≠ 0: mostrar badge "Análisis activo" en naranja junto al título.
|
||||||
|
- Botón "Restablecer" visible solo cuando algún slider ≠ 0.
|
||||||
|
|
||||||
|
### Slider de volumen
|
||||||
|
|
||||||
|
- Label: **"Volumen anual"**
|
||||||
|
- Rango: -50 a +100, paso 10
|
||||||
|
- Valor mostrado: si ≠ 0, mostrar el valor ajustado con delta: `"12.000 ej/año (+20%)"`. Si = 0, mostrar `"12.000 ej/año (valor base)"`.
|
||||||
|
- El valor ajustado = `process.annualFrequency * (1 + volumeAdjust / 100)`, formateado con separadores de miles.
|
||||||
|
|
||||||
|
### Slider de inversión
|
||||||
|
|
||||||
|
- Label: **"Inversión en automatización"**
|
||||||
|
- Rango: -50 a +100, paso 10
|
||||||
|
- Valor mostrado: si ≠ 0, mostrar `"USD 80.000 (+10%)"`. Si = 0, `"USD 80.000 (valor base)"`.
|
||||||
|
- El valor ajustado = `process.automationInvestment * (1 + investmentAdjust / 100)`, formateado con `formatCurrency`.
|
||||||
|
|
||||||
|
### Estructura visual del panel
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────┐
|
||||||
|
│ 📊 Análisis de sensibilidad [●Análisis activo] │ ▾ │
|
||||||
|
├─────────────────────────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ Volumen anual │
|
||||||
|
│ 12.000 ej/año (+20%) │
|
||||||
|
│ ────────────●──────────────── slider │
|
||||||
|
│ -50% 0% +20% +100% │
|
||||||
|
│ │
|
||||||
|
│ Inversión en automatización │
|
||||||
|
│ USD 80.000 (valor base) │
|
||||||
|
│ ──────●──────────────────── slider │
|
||||||
|
│ -50% 0% +100% │
|
||||||
|
│ │
|
||||||
|
│ [Restablecer] │
|
||||||
|
└─────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### Estilos
|
||||||
|
|
||||||
|
- Contenedor: `bg-amber-50 border border-amber-200 rounded-xl p-4 mb-6`
|
||||||
|
- Badge "Análisis activo": pastilla pequeña en naranja InQ (`bg-orange-100 text-orange-700 text-xs font-medium px-2 py-0.5 rounded-full`)
|
||||||
|
- Slider nativo HTML: `<input type="range">` con `accent-color: #F59845`
|
||||||
|
- Botón Restablecer: variante `outline`, tamaño `sm`, solo visible cuando `volumeAdjust !== 0 || investmentAdjust !== 0`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Versión wow — doble línea en `CumulativeSavingsChart`
|
||||||
|
|
||||||
|
**Condición:** implementar si el costo extra está dentro del 40% del tiempo de la versión mínima.
|
||||||
|
|
||||||
|
### Cambios en `CumulativeSavingsChart.tsx`
|
||||||
|
|
||||||
|
Agregar prop opcional:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
interface CumulativeSavingsChartProps {
|
||||||
|
roi: RoiResult // caso ajustado (o base si no hay ajuste)
|
||||||
|
horizonYears: number
|
||||||
|
currency: string
|
||||||
|
investment: number // inversión ajustada (usada para cálculo de valores del eje Y)
|
||||||
|
roiBase?: RoiResult // caso base original — cuando presente, mostrar como línea secundaria
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Cuando `roiBase` está presente (sliders activos):
|
||||||
|
|
||||||
|
1. **Línea secundaria** (caso base): `roiBase.annualSavings * y - investmentOriginal` — dibujada como línea delgada (width: 1.5) en `#94a3b8` (slate-400), tipo `dashed`, sin punto de payback.
|
||||||
|
2. **Área entre las dos líneas**: series de tipo `line` con `areaStyle`, naranja InQ a opacidad 0.08, entre la línea base y la ajustada.
|
||||||
|
3. **Línea principal** (caso ajustado): igual que hoy — naranja InQ, width 2.5, con el marcador de payback si aplica.
|
||||||
|
|
||||||
|
El eje Y debe escalar para acomodar ambas líneas (usar `Math.min` y `Math.max` de todos los valores).
|
||||||
|
|
||||||
|
**Si el costo supera el 40%:** entregar solo la versión mínima (SensitivityPanel funcional, RoiKpiBar actualizado, sin doble línea). Reportar la estimación de tiempo real al director.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Criterios de validación
|
||||||
|
|
||||||
|
- [ ] Panel visible en tab "Comparación + ROI", colapsado por defecto
|
||||||
|
- [ ] Al expandir, se ven dos sliders con labels y valores actualizados en tiempo real
|
||||||
|
- [ ] Mover slider de volumen actualiza los 4 KPIs de ROI en tiempo real (sin delay perceptible)
|
||||||
|
- [ ] Mover slider de inversión actualiza payback y ROI% en tiempo real
|
||||||
|
- [ ] Con ambos sliders en 0, los KPIs son idénticos a los valores originales
|
||||||
|
- [ ] Badge "Análisis activo" aparece cuando algún slider ≠ 0, desaparece al restablecer
|
||||||
|
- [ ] Botón "Restablecer" vuelve ambos sliders a 0 y los KPIs al valor base
|
||||||
|
- [ ] `ComparisonTable` no cambia al mover los sliders (per-ejecución, correcto)
|
||||||
|
- [ ] (Wow) Cuando sliders activos: CumulativeSavingsChart muestra línea base en gris + área sombreada + línea ajustada en naranja
|
||||||
|
- [ ] Tests unitarios del recálculo: al menos 6 casos (volumen +20%, -30%; inversión +50%, -50%; combinado; reset a 0)
|
||||||
|
- [ ] `npm run lint` sin errores
|
||||||
|
- [ ] `npm run build` limpio
|
||||||
|
- [ ] `npm run test` → 542+ tests verdes
|
||||||
|
- [ ] **Validación visual del director sobre el panel en web antes del OK formal**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Archivos a crear o modificar
|
||||||
|
|
||||||
|
**Crear:**
|
||||||
|
- `src/features/report/SensitivityPanel.tsx`
|
||||||
|
- `tests/features/report/sensitivity.test.ts`
|
||||||
|
|
||||||
|
**Modificar:**
|
||||||
|
- `src/features/report/ReportPage.tsx` — estado + adjustedRoi + layout en tab ROI
|
||||||
|
- `src/features/report/CumulativeSavingsChart.tsx` — prop `roiBase` opcional (versión wow)
|
||||||
|
|
||||||
|
**No modificar:**
|
||||||
|
- `domain/roi.ts` — solo consumir, no tocar
|
||||||
|
- `domain/simulation.ts` — no re-simular
|
||||||
|
- Supabase / persistencia — este panel es efímero, vive solo en memoria
|
||||||
|
- `ComparisonTable.tsx` — no recibe el roi ajustado, opera sobre datos per-ejecución
|
||||||
|
- Tests existentes de PDF — no se tocan
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Notas para el agente
|
||||||
|
|
||||||
|
1. El `adjustedRoi` es `null` hasta que `roi` y `process` están cargados — manejar ese caso con el mismo guard que usa `roi` hoy (`!hasAutomatedResult || !roi`).
|
||||||
|
2. Cuando `volumeAdjust === 0 && investmentAdjust === 0`, `adjustedRoi === roi` (mismo objeto por referencia, gracias al early return en el useMemo) — no hay render adicional innecesario.
|
||||||
|
3. El slider HTML nativo es suficiente — no usar ninguna librería de sliders. `accent-color` en línea o via Tailwind `accent-orange-400`.
|
||||||
|
4. El `investment` que pasa a `CumulativeSavingsChart` debe ser el ajustado: `process.automationInvestment * (1 + investmentAdjust / 100)` — de lo contrario el gráfico no arrancará en el punto correcto del eje Y.
|
||||||
|
5. La versión wow agrega props opcionales a `CumulativeSavingsChart` — la firma existente debe seguir siendo válida sin cambios (retrocompatibilidad con los tests existentes que mockean el componente).
|
||||||
Reference in New Issue
Block a user