Fix etapa 3

This commit is contained in:
2026-05-19 22:08:00 -03:00
parent a01380240a
commit 8b6ae5fa64
5 changed files with 351 additions and 40 deletions

View File

@@ -1,5 +1,5 @@
import { useEffect, useMemo, useRef, useState } from 'react'
import { ArrowUpDown, ArrowUp, ArrowDown } from 'lucide-react'
import { ArrowUpDown, ArrowUp, ArrowDown, ChevronRight } from 'lucide-react'
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
import { activityColor } from '@/lib/colors'
import { formatCurrency, formatPercent, formatMinutes } from '@/lib/format'
@@ -26,6 +26,7 @@ function SortIcon({ column, sortKey, dir }: { column: SortKey; sortKey: SortKey;
export function ActivitiesTable({ perActivity, mode, currency, highlightedId, onRowClick }: ActivitiesTableProps) {
const [sortKey, setSortKey] = useState<SortKey>('expectedTotalCost')
const [sortDir, setSortDir] = useState<'asc' | 'desc'>('desc')
const [expandedRows, setExpandedRows] = useState<Set<string>>(new Set())
const rowRefs = useRef(new Map<string, HTMLTableRowElement>())
function handleSort(key: SortKey) {
@@ -37,6 +38,15 @@ export function ActivitiesTable({ perActivity, mode, currency, highlightedId, on
}
}
function toggleRow(activityId: string) {
setExpandedRows(prev => {
const next = new Set(prev)
if (next.has(activityId)) next.delete(activityId)
else next.add(activityId)
return next
})
}
const sorted = useMemo(() => {
return [...perActivity].sort((a, b) => {
const av = a[sortKey]
@@ -73,7 +83,7 @@ export function ActivitiesTable({ perActivity, mode, currency, highlightedId, on
<Table>
<TableHeader className="bg-slate-50">
<tr>
<TableHead className="w-8 text-xs" />
<TableHead className="w-10 text-xs" />
<Th label="Actividad" column="activityName" />
<Th label="Costo directo" column="expectedDirectCost" />
<Th label="Overhead" column="expectedIndirectCost" />
@@ -87,43 +97,99 @@ export function ActivitiesTable({ perActivity, mode, currency, highlightedId, on
{sorted.map((act) => {
const color = activityColor(act, perActivity, mode)
const isHighlighted = act.bpmnElementId === highlightedId
const isExpanded = expandedRows.has(act.activityId)
// Costo fijo esperado = total directo suma de costos de recursos
const resourceTotal = act.resourceCostBreakdown.reduce((s, r) => s + r.cost, 0)
const fixedCostExpected = act.expectedDirectCost - resourceTotal
// Mostrar chevron a menos que no haya absolutamente nada que mostrar
const hasBreakdown = !(act.resourceCostBreakdown.length === 0 && act.expectedDirectCost === 0)
return (
<TableRow
key={act.activityId}
ref={(el) => { if (el) rowRefs.current.set(act.bpmnElementId, el) }}
onClick={() => onRowClick(act.bpmnElementId)}
className={`cursor-pointer ${isHighlighted ? 'bg-primary/5 ring-1 ring-inset ring-primary/20' : ''}`}
data-state={isHighlighted ? 'selected' : undefined}
>
<TableCell className="py-2">
<div
className="w-3 h-3 rounded-sm flex-shrink-0"
style={{ backgroundColor: color }}
/>
</TableCell>
<TableCell className="text-xs font-medium text-slate-800 max-w-48 truncate">
{act.activityName}
</TableCell>
<TableCell className="text-xs font-mono text-slate-600">
{formatCurrency(act.expectedDirectCost, currency)}
</TableCell>
<TableCell className="text-xs font-mono text-slate-500">
{formatCurrency(act.expectedIndirectCost, currency)}
</TableCell>
<TableCell className="text-xs font-mono font-semibold text-slate-800">
{formatCurrency(act.expectedTotalCost, currency)}
</TableCell>
<TableCell className="text-xs font-mono text-slate-600">
{formatPercent(act.percentOfTotal)}
</TableCell>
<TableCell className="text-xs font-mono text-slate-500">
{act.executionTimeMinutes > 0 ? formatMinutes(act.executionTimeMinutes) : '—'}
</TableCell>
<TableCell className="text-xs font-mono text-slate-500">
{formatPercent(act.executionProbability * 100)}
</TableCell>
</TableRow>
<>
<TableRow
key={act.activityId}
ref={(el) => { if (el) rowRefs.current.set(act.bpmnElementId, el) }}
onClick={() => onRowClick(act.bpmnElementId)}
className={`cursor-pointer ${isHighlighted ? 'bg-primary/5 ring-1 ring-inset ring-primary/20' : ''}`}
data-state={isHighlighted ? 'selected' : undefined}
>
{/* Chevron + indicador de color en la misma celda */}
<TableCell className="py-2 pr-1">
<div className="flex items-center gap-1">
{hasBreakdown ? (
<button
onClick={(e) => { e.stopPropagation(); toggleRow(act.activityId) }}
className="text-slate-400 hover:text-slate-600 transition-colors flex-shrink-0"
aria-label={isExpanded ? 'Colapsar desglose' : 'Expandir desglose'}
>
<ChevronRight
className="h-3.5 w-3.5 transition-transform duration-150"
style={{ transform: isExpanded ? 'rotate(90deg)' : 'rotate(0deg)' }}
/>
</button>
) : (
<span className="w-3.5" />
)}
<div
className="w-3 h-3 rounded-sm flex-shrink-0"
style={{ backgroundColor: color }}
/>
</div>
</TableCell>
<TableCell className="text-xs font-medium text-slate-800 max-w-48 truncate">
{act.activityName}
</TableCell>
<TableCell className="text-xs font-mono text-slate-600">
{formatCurrency(act.expectedDirectCost, currency)}
</TableCell>
<TableCell className="text-xs font-mono text-slate-500">
{formatCurrency(act.expectedIndirectCost, currency)}
</TableCell>
<TableCell className="text-xs font-mono font-semibold text-slate-800">
{formatCurrency(act.expectedTotalCost, currency)}
</TableCell>
<TableCell className="text-xs font-mono text-slate-600">
{formatPercent(act.percentOfTotal)}
</TableCell>
<TableCell className="text-xs font-mono text-slate-500">
{act.executionTimeMinutes > 0 ? formatMinutes(act.executionTimeMinutes) : '—'}
</TableCell>
<TableCell className="text-xs font-mono text-slate-500">
{formatPercent(act.executionProbability * 100)}
</TableCell>
</TableRow>
{/* Fila expandida con desglose de costos */}
{isExpanded && (
<TableRow key={`${act.activityId}-breakdown`} className="bg-slate-50/70 hover:bg-slate-50/70">
<TableCell />
<TableCell colSpan={7} className="py-2 pl-4 pr-6">
<div className="space-y-1">
{fixedCostExpected > 0 && (
<div className="flex justify-between text-xs text-slate-500">
<span>Fijo (insumos, licencias)</span>
<span className="font-mono">{formatCurrency(fixedCostExpected, currency)}</span>
</div>
)}
{act.resourceCostBreakdown.map((r) => (
<div key={r.resourceId} className="flex justify-between text-xs text-slate-500">
<span>{r.resourceName}</span>
<span className="font-mono">{formatCurrency(r.cost, currency)}</span>
</div>
))}
{act.resourceCostBreakdown.length === 0 && fixedCostExpected === 0 && (
<p className="text-xs text-slate-400">Sin desglose disponible</p>
)}
{act.resourceCostBreakdown.length === 0 && fixedCostExpected > 0 && (
<p className="text-xs text-slate-400 mt-0.5">Sin recursos asignados costo es 100% fijo</p>
)}
</div>
</TableCell>
</TableRow>
)}
</>
)
})}
</TableBody>

View File

@@ -30,6 +30,8 @@ export function CumulativeSavingsChart({
position: 'insideEndTop',
fontSize: 10,
color: '#16A34A',
padding: [0, 0, 0, 8],
offset: [4, 0],
},
lineStyle: { color: '#16A34A', type: 'dashed', width: 2 },
})