From bae8dc11c76643c8a69cef1f3a388c6535024ebc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcos=20Ben=C3=ADtez?= Date: Wed, 24 Jun 2026 20:06:23 -0300 Subject: [PATCH] --- OBSERVATIONS.md | 7 +- sprints/sprint-6/ETAPA_2_PROMPT.md | 241 +++++++++++++++++++++++++++++ 2 files changed, 247 insertions(+), 1 deletion(-) create mode 100644 sprints/sprint-6/ETAPA_2_PROMPT.md diff --git a/OBSERVATIONS.md b/OBSERVATIONS.md index 0261a71..4ad274c 100644 --- a/OBSERVATIONS.md +++ b/OBSERVATIONS.md @@ -24,7 +24,12 @@ ## 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. --- diff --git a/sprints/sprint-6/ETAPA_2_PROMPT.md b/sprints/sprint-6/ETAPA_2_PROMPT.md new file mode 100644 index 0000000..28fba05 --- /dev/null +++ b/sprints/sprint-6/ETAPA_2_PROMPT.md @@ -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 */} + + +{/* Panel de sensibilidad — colapsado por defecto */} + { setVolumeAdjust(0); setInvestmentAdjust(0) }} +/> + +{/* Tabla de comparación — recibe roi base (per-ejecución, no varía) */} +
+

Comparación por actividad

+ +
+ +{/* Gráficos */} + +``` + +--- + +## 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: `` 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).