feat(sprint-3): biblioteca, recursos TO-BE, ROI mejorado e identidad de marca
Sprint 3 completo — 7 etapas: - Etapa 1-2: biblioteca de procesos con agrupación por cliente (Dexie v4, LibraryPage, library-store, groupId/tags en Process) - Etapa 3: modal de configuración global por proceso (horizonte, frecuencia, inversión, moneda) con validación y persistencia - Etapa 4: panel de recursos rediseñado con soporte de unidades y minutos de utilización (utilizationMinutes + units) - Etapa 5: recursos en escenario automatizado — motor de simulación, UI de edición en ActivityPanel, comparación AS-IS/TO-BE en reporte - Etapa 6: PDF recursos automatizados, automatedAssignedResources requerido, 561 tests pasando - Etapa 7: identidad de marca — AppHeader gradiente naranja/magenta con logo InQuality en 5 páginas, favicon con logo real, logo/marca navegan a home Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useMemo, useRef, useState } from 'react'
|
||||
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'
|
||||
@@ -27,6 +27,8 @@ import { TransparencySection } from './TransparencySection'
|
||||
import { AutomationScenarioNote } from './AutomationScenarioNote'
|
||||
import { MethodologyFooter } from './MethodologyFooter'
|
||||
import type { HeatmapMode } from '@/lib/colors'
|
||||
import { useSimulationStore } from '@/store/simulation-store'
|
||||
import { AppHeader } from '@/components/AppHeader'
|
||||
|
||||
export function ReportPage() {
|
||||
const { processId } = useParams({ from: '/report/$processId' })
|
||||
@@ -35,6 +37,12 @@ export function ReportPage() {
|
||||
|
||||
const { toast } = useToast()
|
||||
const [heatmapMode, setHeatmapMode] = useState<HeatmapMode>('relative')
|
||||
const { lastPersistedExecutedAt, loadLatestForProcess } = useSimulationStore()
|
||||
|
||||
// Cargar executedAt persistido si el usuario llega directo al reporte (sin pasar por workspace)
|
||||
useEffect(() => {
|
||||
if (process) loadLatestForProcess(process.id)
|
||||
}, [process?.id, loadLatestForProcess])
|
||||
|
||||
// Set de bpmnElementIds marcados como automatizables — debe estar antes de los early returns
|
||||
const automatableIds = useMemo(
|
||||
@@ -56,6 +64,14 @@ export function ReportPage() {
|
||||
})
|
||||
}, [simulation, process])
|
||||
|
||||
// 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?.result])
|
||||
|
||||
// Estado y refs para el tab "Actual"
|
||||
const [selectedIdActual, setSelectedIdActual] = useState<string | null>(null)
|
||||
const heatmapRef = useRef<HeatmapCanvasHandle>(null)
|
||||
@@ -219,9 +235,29 @@ export function ReportPage() {
|
||||
// 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 (
|
||||
<TooltipProvider>
|
||||
<div className="min-h-screen bg-slate-50">
|
||||
<AppHeader />
|
||||
{/* Banner stale — cambios sin simular */}
|
||||
{isStale && (
|
||||
<div className="flex items-center justify-between px-6 py-2.5 text-[12px] font-medium"
|
||||
style={{ background: '#fffbeb', borderBottom: '1px solid #f59e0b', color: '#92400e' }}>
|
||||
<div className="flex items-center gap-2">
|
||||
<AlertTriangle className="h-3.5 w-3.5 shrink-0" />
|
||||
Los resultados pueden estar desactualizados — hay cambios sin simular.
|
||||
</div>
|
||||
<button
|
||||
onClick={() => navigate({ to: '/workspace/$processId', params: { processId } })}
|
||||
className="underline hover:no-underline shrink-0 ml-4"
|
||||
>
|
||||
Ir al workspace →
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="max-w-screen-xl mx-auto px-6 py-10">
|
||||
|
||||
{/* ── Header (fuera de los tabs: aplica a todos los escenarios) ──── */}
|
||||
@@ -449,6 +485,7 @@ export function ReportPage() {
|
||||
</div>
|
||||
<ActivitiesTable
|
||||
perActivity={resultAutomated!.perActivity}
|
||||
pairedBreakdown={actualBreakdownById}
|
||||
mode={heatmapMode}
|
||||
currency={process.currency}
|
||||
highlightedId={selectedIdAuto}
|
||||
|
||||
Reference in New Issue
Block a user