Los cambios en reporte de eficiencia
Some checks failed
/ Deploy to Cloudflare Pages (push) Has been cancelled

This commit is contained in:
2026-06-23 17:09:54 -03:00
parent ecfb72c3f1
commit bf200b72f9
5 changed files with 813 additions and 25 deletions

View File

@@ -1,6 +1,7 @@
import { useMemo, useState } from 'react'
import { Bot, ArrowUpDown } from 'lucide-react'
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
import { formatCurrency, formatPercent } from '@/lib/format'
import type { ActivitySimResult } from '@/domain/types'
@@ -12,8 +13,12 @@ interface ComparisonRow {
savings: number
savingsPercent: number
automatable: boolean
expectedSavings: number | null
efficiency: number | null
}
type SortBy = 'savings' | 'expectedSavings' | 'efficiency'
interface ComparisonTableProps {
perActivityActual: ActivitySimResult[]
perActivityAutomated: ActivitySimResult[]
@@ -21,12 +26,46 @@ interface ComparisonTableProps {
currency: string
}
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>
)
}
export function ComparisonTable({
perActivityActual,
perActivityAutomated,
automatableIds,
currency,
}: ComparisonTableProps) {
const [sortBy, setSortBy] = useState<SortBy>('efficiency')
const [sortDir, setSortDir] = useState<'asc' | 'desc'>('desc')
const automatedByActivityId = useMemo(() => {
@@ -36,25 +75,53 @@ export function ComparisonTable({
}, [perActivityAutomated])
const rows: ComparisonRow[] = useMemo(() => {
return perActivityActual
.map((act) => {
const automated = automatedByActivityId.get(act.activityId)
const automatedCost = automated?.expectedTotalCost ?? act.expectedTotalCost
const savings = act.expectedTotalCost - automatedCost
const savingsPercent =
act.expectedTotalCost > 0 ? (savings / act.expectedTotalCost) * 100 : 0
return {
activityId: act.activityId,
activityName: act.activityName,
actualCost: act.expectedTotalCost,
automatedCost,
savings,
savingsPercent,
automatable: automatableIds.has(act.bpmnElementId),
}
})
.sort((a, b) => (b.savings - a.savings) * (sortDir === 'desc' ? 1 : -1))
}, [perActivityActual, automatedByActivityId, automatableIds, sortDir])
const built = perActivityActual.map((act) => {
const automated = automatedByActivityId.get(act.activityId)
const automatedCost = automated?.expectedTotalCost ?? act.expectedTotalCost
const savings = act.expectedTotalCost - automatedCost
const savingsPercent =
act.expectedTotalCost > 0 ? (savings / act.expectedTotalCost) * 100 : 0
const automatable = automatableIds.has(act.bpmnElementId)
const expectedSavings =
automatable && savings > 0 ? savings * act.executionProbability : null
const efficiency =
automatable && savings > 0 && act.executionTimeMinutes > 0
? (savings * act.executionProbability) / act.executionTimeMinutes
: null
return {
activityId: act.activityId,
activityName: act.activityName,
actualCost: act.expectedTotalCost,
automatedCost,
savings,
savingsPercent,
automatable,
expectedSavings,
efficiency,
}
})
return built.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)
})
}, [perActivityActual, automatedByActivityId, automatableIds, sortBy, sortDir])
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
)
if (rows.length === 0) {
return (
@@ -64,6 +131,14 @@ export function ComparisonTable({
)
}
function toggleSort(col: SortBy) {
if (sortBy === col) setSortDir((d) => (d === 'desc' ? 'asc' : 'desc'))
else {
setSortBy(col)
setSortDir('desc')
}
}
return (
<div className="rounded-xl border border-inq-orange/20 overflow-hidden">
<Table>
@@ -75,14 +150,50 @@ export function ComparisonTable({
<TableHead className="text-xs text-right text-amber-800/70 font-medium uppercase tracking-wide">Costo automatizado</TableHead>
<TableHead
className="text-xs text-right cursor-pointer select-none text-amber-800/70 font-medium uppercase tracking-wide hover:text-amber-900"
onClick={() => setSortDir((d) => (d === 'desc' ? 'asc' : 'desc'))}
onClick={() => toggleSort('savings')}
>
<span className="inline-flex items-center justify-end gap-1">
Ahorro
<ArrowUpDown className="h-3 w-3 opacity-40" />
<ArrowUpDown className={`h-3 w-3 ${sortBy === 'savings' ? 'opacity-80' : 'opacity-40'}`} />
</span>
</TableHead>
<TableHead className="text-xs text-right text-amber-800/70 font-medium uppercase tracking-wide">% ahorro</TableHead>
<TableHead
className="text-xs text-right cursor-pointer select-none text-amber-800/70 font-medium uppercase tracking-wide hover:text-amber-900"
onClick={() => toggleSort('expectedSavings')}
>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<span className="inline-flex items-center justify-end gap-1">
Ahorro esperado
<ArrowUpDown className={`h-3 w-3 ${sortBy === 'expectedSavings' ? 'opacity-80' : 'opacity-40'}`} />
</span>
</TooltipTrigger>
<TooltipContent side="top" className="max-w-52 text-xs text-center">
Ahorro por instancia ponderado por probabilidad de ejecución de la actividad
</TooltipContent>
</Tooltip>
</TooltipProvider>
</TableHead>
<TableHead
className="text-xs text-right cursor-pointer select-none text-amber-800/70 font-medium uppercase tracking-wide hover:text-amber-900"
onClick={() => toggleSort('efficiency')}
>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<span className="inline-flex items-center justify-end gap-1">
Eficiencia
<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">
Ahorro esperado dividido por tiempo de ejecución prioriza actividades con mayor retorno por minuto de proceso
</TooltipContent>
</Tooltip>
</TooltipProvider>
</TableHead>
</tr>
</TableHeader>
<TableBody>
@@ -124,6 +235,16 @@ export function ComparisonTable({
? (row.savings > 0 ? '+' : '') + formatPercent(row.savingsPercent)
: '—'}
</TableCell>
<MetricCell
value={row.expectedSavings}
max={maxExpectedSavings}
format={(v) => formatCurrency(v, currency)}
/>
<MetricCell
value={row.efficiency}
max={maxEfficiency}
format={(v) => v.toFixed(2)}
/>
</TableRow>
)
})}