Files
inq-roi-simulador-web/sprints/sprint-6/ETAPA_2_PROMPT.md
Marcos Benítez bae8dc11c7
Some checks failed
/ Deploy to Cloudflare Pages (push) Has been cancelled
2026-06-24 20:06:23 -03:00

11 KiB

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:

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:

// 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:

{/* 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

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:

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).