Los cambios en reporte de eficiencia
Some checks failed
/ Deploy to Cloudflare Pages (push) Has been cancelled
Some checks failed
/ Deploy to Cloudflare Pages (push) Has been cancelled
This commit is contained in:
@@ -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>
|
||||
)
|
||||
})}
|
||||
|
||||
Reference in New Issue
Block a user