import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useParams, useNavigate } from '@tanstack/react-router' import { AlertTriangle, Home, Info } from 'lucide-react' import { Button } from '@/components/ui/button' import { Separator } from '@/components/ui/separator' import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs' import { TooltipProvider, Tooltip, TooltipTrigger, TooltipContent } from '@/components/ui/tooltip' import { useToast } from '@/components/ui/use-toast' import { AppFooter } from '@/components/AppFooter' import { calculateRoi } from '@/domain/roi' import { useReportData } from './useReportData' import { ReportHeader } from './ReportHeader' import { KpiBar } from './KpiBar' import { RoiKpiBar } from './RoiKpiBar' import { HeatmapCanvas, type HeatmapCanvasHandle } from './HeatmapCanvas' import { HeatmapLegend } from './HeatmapLegend' import { HeatmapModeToggle } from './HeatmapModeToggle' import { WarningsBanner } from './WarningsBanner' import { TopActivitiesChart } from './TopActivitiesChart' import { CostCompositionChart } from './CostCompositionChart' import { CostByResourceChart } from './CostByResourceChart' import { ActivitiesTable } from './ActivitiesTable' import { ComparisonTable } from './ComparisonTable' import { CumulativeSavingsChart } from './CumulativeSavingsChart' import { SensitivityPanel } from './SensitivityPanel' import { TopSavingsChart } from './TopSavingsChart' import { TransparencySection } from './TransparencySection' import { AutomationScenarioNote } from './AutomationScenarioNote' import { MethodologyFooter } from './MethodologyFooter' import { useHeatmapMode } from '@/lib/hooks/useHeatmapMode' import { useSimulationStore } from '@/store/simulation-store' import { AppHeader } from '@/components/AppHeader' export function ReportPage() { const { processId } = useParams({ from: '/report/$processId' }) const navigate = useNavigate() const { process, simulation, activities, resources, isLoading, error } = useReportData(processId) const { toast } = useToast() const [heatmapMode, setHeatmapMode] = useHeatmapMode() const { lastPersistedExecutedAt, loadLatestForProcess } = useSimulationStore() // Cargar executedAt persistido si el usuario llega directo al reporte (sin pasar por workspace) // Intencional: dep [process?.id] — solo re-ejecutar cuando cambia el proceso, no en cada // re-render por cambios de nombre/cliente/etc. del objeto process. useEffect(() => { if (process) loadLatestForProcess(process.id) // eslint-disable-next-line react-hooks/exhaustive-deps }, [process?.id, loadLatestForProcess]) // Set de bpmnElementIds marcados como automatizables — debe estar antes de los early returns const automatableIds = useMemo( () => new Set(activities.filter((a) => a.automatable).map((a) => a.bpmnElementId)), [activities] ) // ROI — calculado solo cuando hay ambos resultados const roi = useMemo(() => { const result = simulation?.result const resultAutomated = simulation?.resultAutomated if (!result || !resultAutomated || !process) return null return calculateRoi({ costPerExecutionActual: result.totalCost, costPerExecutionAutomated: resultAutomated.totalCost, annualFrequency: process.annualFrequency, analysisHorizonYears: process.analysisHorizonYears, automationInvestment: process.automationInvestment, }) }, [simulation, process]) // Sliders del panel de sensibilidad — efímero, no persiste (Sprint 6 Etapa 2) const [volumeAdjust, setVolumeAdjust] = useState(0) // entero: -50 a +100 const [investmentAdjust, setInvestmentAdjust] = useState(0) // entero: -50 a +100 // ROI ajustado por sliders de sensibilidad — recalcula solo los agregados anuales, // sin tocar costPerExecutionActual ni costPerExecutionAutomated (son estables, no se re-simula). const adjustedRoi = useMemo(() => { if (!roi || !simulation?.result || !simulation?.resultAutomated || !process) return null if (volumeAdjust === 0 && investmentAdjust === 0) return roi // sin cambios → mismo objeto return calculateRoi({ costPerExecutionActual: simulation.result.totalCost, costPerExecutionAutomated: simulation.resultAutomated.totalCost, annualFrequency: process.annualFrequency * (1 + volumeAdjust / 100), analysisHorizonYears: process.analysisHorizonYears, automationInvestment: process.automationInvestment * (1 + investmentAdjust / 100), }) }, [roi, simulation, process, volumeAdjust, investmentAdjust]) // Breakdown del escenario actual, indexado por activityId, para comparación lado a lado en tab Automatizado const actualBreakdownById = useMemo(() => { if (!simulation?.result) return undefined return Object.fromEntries( simulation.result.perActivity.map((a) => [a.activityId, a.resourceCostBreakdown]) ) }, [simulation]) // Estado y refs para el tab "Actual" const [selectedIdActual, setSelectedIdActual] = useState(null) const heatmapRef = useRef(null) const heatmapSectionRef = useRef(null) const tableSectionRef = useRef(null) // Estado y refs para el tab "Automatizado" const [selectedIdAuto, setSelectedIdAuto] = useState(null) const heatmapRefAuto = useRef(null) const heatmapSectionRefAuto = useRef(null) const tableSectionRefAuto = useRef(null) const [isExportingPdf, setIsExportingPdf] = useState(false) const [isExportingCsv, setIsExportingCsv] = useState(false) const [activeTab, setActiveTab] = useState<'actual' | 'automatizado' | 'roi'>('actual') // Tab "Actual" — cross-linking heatmap ↔ tabla const handleHeatmapClickActual = useCallback((bpmnElementId: string) => { setSelectedIdActual(bpmnElementId) setTimeout(() => { tableSectionRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' }) }, 50) }, []) const handleTableRowClickActual = useCallback((bpmnElementId: string) => { setSelectedIdActual(bpmnElementId) heatmapRef.current?.highlightElement(bpmnElementId) setTimeout(() => { heatmapSectionRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' }) }, 50) }, []) // Tab "Automatizado" — cross-linking heatmap ↔ tabla const handleHeatmapClickAuto = useCallback((bpmnElementId: string) => { setSelectedIdAuto(bpmnElementId) setTimeout(() => { tableSectionRefAuto.current?.scrollIntoView({ behavior: 'smooth', block: 'start' }) }, 50) }, []) const handleTableRowClickAuto = useCallback((bpmnElementId: string) => { setSelectedIdAuto(bpmnElementId) heatmapRefAuto.current?.highlightElement(bpmnElementId) setTimeout(() => { heatmapSectionRefAuto.current?.scrollIntoView({ behavior: 'smooth', block: 'start' }) }, 50) }, []) const handleExportPdf = useCallback(async () => { if (!process || !simulation || isExportingPdf) return setIsExportingPdf(true) try { let heatmapImageData: string | null = null if (activeTab === 'actual') { heatmapImageData = await heatmapRef.current?.captureImage() ?? null } else if (activeTab === 'automatizado') { heatmapImageData = await heatmapRefAuto.current?.captureImage() ?? null } const { exportToPdf } = await import('@/lib/export/pdf-export') await exportToPdf({ process, simulation, resources, heatmapImageData, tab: activeTab, activities }) toast({ title: 'PDF generado', description: 'El reporte se descargó correctamente.' }) } catch (err) { console.error('PDF export error:', err) toast({ title: 'Error al generar el PDF', description: String(err), variant: 'destructive' }) } finally { setIsExportingPdf(false) } }, [process, simulation, resources, isExportingPdf, toast, activeTab, activities]) const handleExportCsv = useCallback(async () => { if (!process || !simulation || isExportingCsv) return setIsExportingCsv(true) try { const { exportToCsv } = await import('@/lib/export/csv-export') exportToCsv({ process, simulation, resources, tab: activeTab, activities }) toast({ title: 'CSV generado', description: 'Los datos se descargaron correctamente.' }) } catch (err) { console.error('CSV export error:', err) toast({ title: 'Error al exportar CSV', description: String(err), variant: 'destructive' }) } finally { setIsExportingCsv(false) } }, [process, simulation, resources, isExportingCsv, toast, activeTab, activities]) // ── Loading ─────────────────────────────────────────────────────────────── if (isLoading) { return (
{[0, 1, 2, 3].map((i) => (
))}
{[0, 1, 2, 3].map((i) => (
))}
) } // ── Error ───────────────────────────────────────────────────────────────── if (error || !process || !simulation) { return (

No se pudo cargar el reporte

{error ?? 'Simulá el proceso desde el workspace para generar el reporte.'}

) } const result = simulation.result const resultAutomated = simulation.resultAutomated ?? null const hasAutomatedResult = resultAutomated !== null // Tabs de escenarios se habilitan solo cuando hay resultado automatizado Y hay actividades // marcadas como automatizables. Sin automatizables, los tabs muestran datos idénticos al actual. const hasAutomatedScenario = hasAutomatedResult && automatableIds.size > 0 const isStale = Boolean(process && process.updatedAt > (lastPersistedExecutedAt ?? 0)) return (
{/* Banner stale — cambios sin simular */} {isStale && (
Los resultados pueden estar desactualizados — hay cambios sin simular.
)}
{/* ── Header (fuera de los tabs: aplica a todos los escenarios) ──── */} navigate({ to: '/workspace/$processId', params: { processId } })} onExportPdf={handleExportPdf} onExportCsv={handleExportCsv} isExportingPdf={isExportingPdf} isExportingCsv={isExportingCsv} /> {/* ── Tabs ─────────────────────────────────────────────────────── */} setActiveTab(v as typeof activeTab)}> Actual Automatizado {!hasAutomatedScenario && ( {!hasAutomatedResult ? 'Re-simulá el proceso para generar el escenario automatizado' : 'Marcá al menos una actividad como automatizable en el workspace para ver este escenario'} )} Comparación + ROI {!hasAutomatedScenario && ( {!hasAutomatedResult ? 'Re-simulá el proceso para generar el análisis de ROI' : 'Marcá al menos una actividad como automatizable en el workspace para calcular el ROI'} )} {/* ──────────────── TAB: ACTUAL ──────────────────────────────── */}

Mapa de calor de costos

Hacé click en una actividad para ver su detalle en la tabla

Análisis de composición

Top actividades por costo

Composición directo / overhead

Costo por tipo de recurso

Detalle por actividad ({result.perActivity.length} actividades)

{/* ──────────────── TAB: AUTOMATIZADO ───────────────────────── */} {!hasAutomatedResult ? ( ) : ( <>

Mapa de calor — Escenario Automatizado

Hacé click en una actividad para ver su detalle en la tabla

Análisis de composición

Top actividades por costo

Composición directo / overhead

Costo por tipo de recurso

Detalle por actividad — Automatizado ({resultAutomated!.perActivity.length} actividades)

)}
{/* ──────────────── TAB: COMPARACIÓN + ROI ──────────────────── */} {!hasAutomatedResult || !roi ? ( ) : ( <> {/* KPIs de ROI — actualizados por sliders de sensibilidad */} {/* Panel de sensibilidad — colapsado por defecto */} { setVolumeAdjust(0); setInvestmentAdjust(0) }} /> {/* Tabla de comparación actividad por actividad — per-ejecución, no varía con los sliders */}

Comparación por actividad

{/* Gráficos: ahorro acumulado + top ahorros */}

Ahorro acumulado neto ({process.analysisHorizonYears} años)

Top actividades por ahorro

{/* Transparencia metodológica (condicional) */}
)}
) } // ── Componente auxiliar: aviso cuando falta re-simular ─────────────────────── function NeedsResimulate({ processId }: { processId: string }) { const navigate = useNavigate() return (

Esta simulación no incluye el escenario automatizado

Volvé al workspace y presioná "Simular" para generar ambos escenarios.

) }