Files
inq-roi-simulador-web/sprints/sprint-5/ETAPA_5_PROMPT.md
Marcos Benítez bf200b72f9
Some checks failed
/ Deploy to Cloudflare Pages (push) Has been cancelled
Los cambios en reporte de eficiencia
2026-06-23 17:09:54 -03:00

9.0 KiB
Raw Permalink Blame History

Etapa 5 — Columna "Eficiencia de automatización" en ComparisonTable

Sprint: 5
Fecha: 2026-06-23
Alcance: reporte web únicamente. PDF explícitamente fuera del scope (ver decisión al final).


Contexto

ComparisonTable (src/features/report/ComparisonTable.tsx) muestra actualmente 5 columnas: icono automatable | Actividad | Costo actual | Costo automatizado | Ahorro | % ahorro.

La tabla ordena por ahorro descendente por defecto. Falta una métrica que permita rankear actividades por valor de automatización — no quién ahorra más en absoluto, sino cuál tiene mejor retorno considerando su peso en el proceso.


Dos métricas (A/B para focus group con clientes)

Se muestran dos columnas nuevas, una al lado de la otra. La hipótesis es validar con clientes reales cuál agrega más valor en una sesión de consultoría:

Columna Fórmula Interpretación
Ahorro esperado ahorro$ × executionProbability Cuánto se ahorra en promedio por instancia del proceso, ponderando la probabilidad de que la actividad se ejecute
Eficiencia (ahorro$ × executionProbability) / executionTimeMinutes Ahorro por minuto de proceso — prioriza actividades con alto ahorro relativo a su costo de tiempo

Ambos datos ya existen en ActivitySimResult:

  • ahorro$ = actualCost - automatedCost (ya calculado en ComparisonRow)
  • executionProbability = ActivitySimResult.executionProbability
  • executionTimeMinutes = ActivitySimResult.executionTimeMinutes

Reglas de null (aplican a ambas columnas):

  • Si la actividad no es automatable → null (mostrar "—")
  • Si ahorro$ <= 0null
  • Para "Eficiencia": si además executionTimeMinutes === 0null

Nota de modelo: executionProbability es la probabilidad de ejecución de la actividad en una instancia del proceso (calculada por el motor BPMN a partir de los gateways), no la probabilidad de éxito de la automatización. Es el dato disponible hoy. En un eventual v2 se podría agregar una probabilidad de éxito por actividad.


Implementación

1. Agregar las dos métricas al tipo ComparisonRow

interface ComparisonRow {
  // ... campos existentes ...
  expectedSavings: number | null   // ahorro$ × executionProbability
  efficiency: number | null        // (ahorro$ × executionProbability) / executionTimeMinutes
}

En el cálculo de rows (useMemo):

const expectedSavings =
  automatable && savings > 0
    ? savings * act.executionProbability
    : null

const efficiency =
  automatable && savings > 0 && act.executionTimeMinutes > 0
    ? (savings * act.executionProbability) / act.executionTimeMinutes
    : null

act es ActivitySimResultexecutionProbability y executionTimeMinutes ya están disponibles en perActivityActual.

2. Cambiar el estado de sort

Reemplazar const [sortDir, setSortDir] = useState<'asc' | 'desc'>('desc') por:

const [sortBy, setSortBy]   = useState<'savings' | 'expectedSavings' | 'efficiency'>('efficiency')
const [sortDir, setSortDir] = useState<'asc' | 'desc'>('desc')

Función de sort:

.sort((a, b) => {
  const val = (row: ComparisonRow) => {
    if (sortBy === 'efficiency')     return row.efficiency ?? -Infinity
    if (sortBy === 'expectedSavings') return row.expectedSavings ?? -Infinity
    return row.savings
  }
  return (val(b) - val(a)) * (sortDir === 'desc' ? 1 : -1)
})

Filas con valor null siempre al final.

3. Headers de columna con sort y tooltip

Importar Tooltip, TooltipContent, TooltipProvider, TooltipTrigger desde @/components/ui/tooltip.

Patrón para cada columna nueva (repetir para "Ahorro esperado" y "Eficiencia"):

<TableHead
  className="text-xs text-right cursor-pointer text-amber-800/70 font-medium uppercase tracking-wide hover:text-amber-900"
  onClick={() => {
    const col = 'efficiency' // o 'expectedSavings'
    if (sortBy === col) setSortDir(d => d === 'desc' ? 'asc' : 'desc')
    else { setSortBy(col); setSortDir('desc') }
  }}
>
  <TooltipProvider>
    <Tooltip>
      <TooltipTrigger asChild>
        <span className="inline-flex items-center justify-end gap-1">
          Eficiencia {/* o "Ahorro esperado" */}
          <ArrowUpDown className={`h-3 w-3 ${sortBy === 'efficiency' ? 'opacity-80' : 'opacity-40'}`} />
        </span>
      </TooltipTrigger>
      <TooltipContent side="top" className="max-w-52 text-xs text-center">
        {/* Para "Ahorro esperado": */}
        Ahorro por instancia ponderado por probabilidad de ejecución de la actividad
        {/* Para "Eficiencia": */}
        Ahorro esperado dividido por tiempo de ejecución  prioriza actividades con mayor retorno por minuto de proceso
      </TooltipContent>
    </Tooltip>
  </TooltipProvider>
</TableHead>

La columna "Ahorro" existente también debe quedar clickeable para sort (mismo patrón, sortBy === 'savings').

4. Celdas — versión wow: barra de progreso proporcional

Calcular máximos para cada métrica (dentro del useMemo de rows, después del sort):

const maxExpectedSavings = Math.max(
  ...rows.filter(r => r.expectedSavings !== null).map(r => r.expectedSavings as number), 0
)
const maxEfficiency = Math.max(
  ...rows.filter(r => r.efficiency !== null).map(r => r.efficiency as number), 0
)

Componente reutilizable para la celda con barra (puede ser una función local en el archivo):

function MetricCell({ value, max, format }: { value: number | null; max: number; format: (v: number) => string }) {
  if (value === null) return <TableCell className="text-xs text-right"><span className="text-slate-300"></span></TableCell>
  return (
    <TableCell className="text-xs text-right">
      <div className="flex items-center justify-end gap-2">
        <div className="w-14 h-1.5 rounded-full bg-slate-100 overflow-hidden">
          <div
            className="h-full rounded-full bg-gradient-to-r from-amber-400 to-orange-500"
            style={{ width: `${max > 0 ? (value / max) * 100 : 0}%` }}
          />
        </div>
        <span className="font-mono text-slate-700 text-xs w-14 text-right">
          {format(value)}
        </span>
      </div>
    </TableCell>
  )
}

Formato de valores:

  • "Ahorro esperado": formatCurrency(value, currency) — mismo helper que el resto de la tabla
  • "Eficiencia": value.toFixed(2) + sufijo implícito (sin unidad visible, el tooltip explica)

5. Tests unitarios

Crear tests/features/report/efficiency.test.ts:

// Casos básicos para ambas métricas
// expectedSavings = savings × executionProbability
// efficiency = expectedSavings / executionTimeMinutes

// Edge cases:
// - savings ≤ 0 → null en ambas
// - automatable = false → null en ambas
// - executionTimeMinutes = 0 → efficiency null, expectedSavings puede ser válido
// - executionProbability = 0 → null en ambas (resultado sería 0, no rankeable)

// Ranking: filas nulas siempre al final del sort
// Sort por 'efficiency': fila nula < fila con efficiency 0.01

Decisión de producto — focus group

Las dos columnas se implementan para validar con clientes en talleres cuál agrega más valor en contexto real de consultoría. No se elige una antes del focus group.

Cuando haya feedback suficiente, la siguiente decisión puede ser: mantener las dos, quedarse con una, o fusionarlas en una sola columna con toggle. Esa decisión es post Sprint 5.


Scope estricto — lo que NO entra

  • PDF: la tabla de comparación en PDF ya tiene 6 columnas en 138mm de ancho de contenido. Agregar una 7ª columna con barra de progreso no es viable en portrait sin truncar nombres de actividades. La columna de eficiencia queda fuera del PDF en esta etapa.
  • Cambios en el modelo de datos (ActivitySimResult, types.ts): todos los datos necesarios ya existen.
  • Cambios en el motor de simulación (simulation.ts): nada cambia allá.
  • Cambios en ReportPage.tsx (salvo si hay props nuevas que pasar, que no hay).
  • Cambios en pdf-sections-roi.ts ni en buildComparisonTableConfig.

Criterios de validación

  • Dos columnas nuevas visibles: "Ahorro esperado" y "Eficiencia", alineadas a la derecha
  • Actividades no automatizables muestran "—" en ambas columnas
  • Sort por defecto: mayor eficiencia primero (desc)
  • Click en cada columna activa el sort por esa métrica; click nuevamente alterna asc/desc
  • El ícono ArrowUpDown es más opaco en la columna de sort activa
  • Barra de progreso proporcional en ambas columnas: el máximo = 100% del ancho
  • Tooltip aparece al hacer hover en el header de cada columna nueva
  • Tests unitarios de ambas fórmulas en Vitest: verdes, incluyendo edge cases
  • npm run lint sin errores
  • npm run build limpio
  • Validación visual del director antes del OK formal

Archivos a modificar

  1. src/features/report/ComparisonTable.tsx — todo el trabajo de la feature
  2. tests/features/report/efficiency.test.ts (nuevo) — tests unitarios de la fórmula

No modificar ningún otro archivo.