Fix etapa 3
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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 },
|
||||
})
|
||||
|
||||
@@ -24,15 +24,15 @@ function classifyElement(type: string): BpmnElementCategory {
|
||||
// Emoji ⚡ como símbolo provisional — reemplazo por <Bot /> agendado Sprint 2-3.
|
||||
function makeBadgeHtml(): string {
|
||||
return `<div class="automatable-badge" title="Actividad marcada como automatizable" style="
|
||||
width:14px;height:14px;
|
||||
width:20px;height:20px;
|
||||
border-radius:50%;
|
||||
background:var(--inq-orange);
|
||||
border:1.5px solid white;
|
||||
box-shadow:0 1px 2px rgba(0,0,0,0.10);
|
||||
display:flex;align-items:center;justify-content:center;
|
||||
color:white;
|
||||
font-size:8px;font-weight:700;
|
||||
line-height:14px;
|
||||
font-size:11px;font-weight:700;
|
||||
line-height:20px;
|
||||
cursor:default;
|
||||
pointer-events:none;
|
||||
z-index:10;
|
||||
|
||||
Reference in New Issue
Block a user