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):
RoiKpiBar— recibeadjustedRoi ?? roien lugar deroiSensitivityPanel— nuevo, debajo de los KPIs, colapsado por defectoComparisonTable— sin cambio (per-ejecución, no varía con los sliders)- Gráficos:
CumulativeSavingsChartrecibeadjustedRoi(yroicomo 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 conformatCurrency.
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">conaccent-color: #F59845 - Botón Restablecer: variante
outline, tamañosm, solo visible cuandovolumeAdjust !== 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):
- Línea secundaria (caso base):
roiBase.annualSavings * y - investmentOriginal— dibujada como línea delgada (width: 1.5) en#94a3b8(slate-400), tipodashed, sin punto de payback. - Área entre las dos líneas: series de tipo
lineconareaStyle, naranja InQ a opacidad 0.08, entre la línea base y la ajustada. - 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
ComparisonTableno 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 lintsin erroresnpm run buildlimpionpm 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.tsxtests/features/report/sensitivity.test.ts
Modificar:
src/features/report/ReportPage.tsx— estado + adjustedRoi + layout en tab ROIsrc/features/report/CumulativeSavingsChart.tsx— proproiBaseopcional (versión wow)
No modificar:
domain/roi.ts— solo consumir, no tocardomain/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
- El
adjustedRoiesnullhasta queroiyprocessestán cargados — manejar ese caso con el mismo guard que usaroihoy (!hasAutomatedResult || !roi). - Cuando
volumeAdjust === 0 && investmentAdjust === 0,adjustedRoi === roi(mismo objeto por referencia, gracias al early return en el useMemo) — no hay render adicional innecesario. - El slider HTML nativo es suficiente — no usar ninguna librería de sliders.
accent-coloren línea o via Tailwindaccent-orange-400. - El
investmentque pasa aCumulativeSavingsChartdebe ser el ajustado:process.automationInvestment * (1 + investmentAdjust / 100)— de lo contrario el gráfico no arrancará en el punto correcto del eje Y. - 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).