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>
11 KiB
Sprint 3 — Etapa 4: Indicador stale + zoom BPMN
Contexto
Sos Claude Code trabajando en InQ ROI. Las reglas de implementación están en simulador-web/CLAUDE.md. El brief del sprint está en sprints/sprint-3/BRIEF.md.
Estado al iniciar esta etapa:
- Etapas 1-3 completadas. Biblioteca de procesos operativa. Tags y búsqueda funcionando.
SettingsPagedisponible. process-store.tsexpone:updateActivity,updateGateway,updateResource,addResource,deleteResource,updateProcess. Ninguna de estas acciones actualizaprocess.updatedAtexceptoupdateProcess.simulation-store.tstienelastSimulatedAt: Date | null(in-memory, se pierde al recargar) ypersistResults(guarda en IndexedDB conexecutedAt: Date.now()).BpmnCanvas.tsx: viewer bpmn-js puro, sin toolbar, retorna un<div>que es el contenedor del canvas. Ya llamacanvas.zoom('fit-viewport', 'auto')al importar el XML.
Esta etapa agrega dos features independientes:
- Indicador stale — aviso persistente cuando el proceso tiene cambios sin simular.
- Botón fit-viewport — encuadrar el diagrama BPMN en cualquier momento.
Objetivo
D.1 — Indicador de simulación desactualizada
Cuando el usuario modifica actividades, recursos o gateways sin volver a simular, el workspace muestra un indicador visible y persistente (sobrevive a recargas de página).
D.2 — Botón fit-viewport en BpmnCanvas
Un botón flotante sobre el canvas que ejecuta canvas.zoom('fit-viewport'). Detectado en validación Sprint 2 como necesidad real en talleres con diagramas grandes.
Arquitectura — leer antes de implementar
Dependencias entre stores (no crear dependencias circulares)
process-store.ts
↑ importado por
simulation-store.ts (excepción documentada en el archivo)
Para evitar dependencia circular, process-store.ts NO debe importar simulation-store.ts. La coordinación entre stores la hace WorkspacePage.
Flujo correcto para el indicador stale
process-store.ts— bumpprocess.updatedAten mutaciones de actividades/gateways/recursos.simulation-store.ts— agregarlastPersistedExecutedAt: number | null(cargado desde IndexedDB, sobrevive recargas) y la acciónloadLatestForProcess(processId).WorkspacePage— orquesta: llamaloadLatestForProcesscuando el proceso está cargado, y computaisStalecomparando los dos valores.
Cambios autorizados y arquitectura
src/store/process-store.ts
Agregar bump de process.updatedAt en todas las acciones que mutan datos del proceso sin ser updateProcess:
// Patrón a aplicar en: updateActivity, updateGateway, updateResource, addResource, deleteResource
// (updateProcess ya actualiza updatedAt correctamente — no modificar)
// Ejemplo — updateActivity (aplicar el mismo patrón a las demás):
updateActivity: async (activity) => {
await activityRepo.save(activity)
const current = get().currentProcess
if (current) {
const updatedProcess = { ...current, updatedAt: Date.now() }
await processRepo.save(updatedProcess)
set((state) => ({
activities: state.activities.map((a) => (a.id === activity.id ? activity : a)),
currentProcess: updatedProcess,
}))
} else {
set((state) => ({
activities: state.activities.map((a) => (a.id === activity.id ? activity : a)),
}))
}
},
Nota: la suscripción en simulation-store.ts ya llama invalidateResults() cuando currentProcess cambia. Esto es correcto y esperado — no modificar esa suscripción.
src/store/simulation-store.ts
Agregar campo y acción para la comparación persistente:
interface SimulationState {
// ... campos existentes ...
lastPersistedExecutedAt: number | null // 🆕 executedAt de la última sim guardada en IndexedDB
loadLatestForProcess: (processId: string) => Promise<void> // 🆕
}
Implementación:
lastPersistedExecutedAt: null,
loadLatestForProcess: async (processId) => {
const sim = await simulationRepo.getLatestByProcess(processId)
set({ lastPersistedExecutedAt: sim?.executedAt ?? null })
},
Actualizar también persistResults para mantener el campo sincronizado:
persistResults: async (processId, actual, automated) => {
const executedAt = Date.now()
await simulationRepo.save({
id: uuidv4(),
processId,
executedAt,
result: actual,
resultAutomated: automated,
})
set({
resultActual: actual,
resultAutomated: automated,
lastSimulatedAt: new Date(),
lastPersistedExecutedAt: executedAt, // 🆕
error: null,
})
},
Y limpiar en reset:
reset: () => set({ ..., lastPersistedExecutedAt: null })
src/features/workspace/WorkspacePage.tsx
1 — Orquestación (nuevo useEffect)
Después de que el proceso se carga, cargar el último executedAt persistido:
const { loadLatestForProcess, lastPersistedExecutedAt } = useSimulationStore()
useEffect(() => {
if (currentProcess) {
loadLatestForProcess(currentProcess.id)
}
}, [currentProcess?.id, loadLatestForProcess])
2 — Cómputo del indicador
const isStale = Boolean(
currentProcess &&
currentProcess.updatedAt > (lastPersistedExecutedAt ?? 0)
)
3 — UI del indicador
Mostrar un badge sutil junto al botón Simular cuando isStale === true. El botón Simular ya existe en el header del workspace.
Diseño del indicador:
- Chip naranja claro: fondo
#fff3e6, borde#F59845, texto#7a3e00, íconoAlertCircle(Lucide,h-3 w-3). - Texto:
"Hay cambios sin simular". - Ubicación: inline junto al botón Simular, o como fila adicional debajo del header — a criterio del agente según el layout actual (Nivel 1).
- Visible solo cuando
isStale === true. Desaparece al completar la simulación (porquepersistResultsactualizalastPersistedExecutedAt).
src/features/workspace/BpmnCanvas.tsx
Agregar botón flotante de fit-viewport. El canvas actualmente retorna:
return <div ref={containerRef} className={className} style={{ width: '100%', height: '100%' }} />
Cambiar a wrapper con botón superpuesto:
return (
<div className={className} style={{ position: 'relative', width: '100%', height: '100%' }}>
<div ref={containerRef} style={{ width: '100%', height: '100%' }} />
<button
onClick={() => {
const canvas = viewerRef.current?.get('canvas') as any
canvas?.zoom('fit-viewport')
}}
title="Encuadrar diagrama"
style={{
position: 'absolute',
bottom: 12,
right: 12,
width: 32,
height: 32,
borderRadius: 6,
background: 'hsl(var(--background))',
border: '1px solid hsl(var(--border))',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
cursor: 'pointer',
boxShadow: '0 1px 3px rgba(0,0,0,0.1)',
zIndex: 10,
}}
>
<Maximize2 className="h-4 w-4 text-muted-foreground" />
</button>
</div>
)
Importar Maximize2 desde lucide-react.
Versión con wow (recomendada si costo < 40% del mínimo)
Banner en ReportPage:
En src/features/report/ReportPage.tsx, si el proceso tiene cambios sin simular, mostrar un banner sutil en la parte superior del reporte:
⚠️ Los resultados pueden estar desactualizados — hay cambios sin simular.
[Ir al workspace →]
Diseño: fondo #fffbeb, borde #f59e0b, texto #92400e (amber). Full-width, 40px de alto máximo. Link "Ir al workspace →" navega a /workspace/$processId.
ReportPage ya usa useProcessStore para leer currentProcess. Necesita también useSimulationStore().lastPersistedExecutedAt para el cálculo de isStale.
❌ Lo que NO entra en esta etapa
- Cambios en
ActivityPanel.tsx,ResourcesPanel.tsx,GatewayPanel.tsx— no tocar automatedAssignedResources— sigue siendo opcional, no promover (Etapa 5)- Cambios en el motor de simulación (
domain/simulation.ts) - Export PDF/CSV
- Biblioteca o library-store
- Tags, settings
Verificación antes de entregar
# 1. Build limpio
npm run build
# 2. Tests (todos los previos deben seguir verdes)
npm run test
# 3. Control de scope
git diff --name-only HEAD
El diff debe mostrar únicamente:
src/store/process-store.tssrc/store/simulation-store.tssrc/features/workspace/WorkspacePage.tsxsrc/features/workspace/BpmnCanvas.tsxsrc/features/report/ReportPage.tsx(solo si se implementó wow)
Si aparece cualquier otro archivo de src/features/ que no sea workspace/ o report/: revertir y reportar.
Verificación funcional mandatoria
Antes de reportar, verificar manualmente:
- Stale persiste en recarga: modificar una actividad → recargar página → el indicador sigue visible → simular → desaparece.
- Sin sim previa: proceso recién importado sin simular nunca → indicador visible (ya que
updatedAt > 0 > null ?? 0). - Fit-viewport: abrir un diagrama complejo → click en el botón → el canvas se encuadra.
- Sin falsos positivos: proceso recién simulado sin cambios posteriores → indicador ausente.
Criterios de aceptación
- Modificar actividad/recurso/gateway actualiza
process.updatedAten IndexedDB. - Indicador "Hay cambios sin simular" aparece en WorkspacePage al detectar
isStale. - Indicador desaparece al simular exitosamente.
- Indicador persiste al recargar la página (comparación contra IndexedDB, no solo memoria).
- Proceso sin historial de simulación → indicador visible.
- Botón fit-viewport en BpmnCanvas encuadra el diagrama correctamente.
- Build sin errores TypeScript.
- Todos los tests previos siguen verdes.
- Validación visual del director aprobada.
Reporte esperado al terminar
- Resultado de
git diff --name-only HEAD. - Resultado de
npm run buildynpm run test. - Confirmación de la verificación funcional (4 casos).
- Si se implementó el banner en ReportPage (wow): declararlo.
- Cualquier decisión de iniciativa tomada (Nivel 1/2/3).
- Si hubo archivo fuera del scope: declararlo con justificación.
Referencias
sprints/sprint-3/BRIEF.md— sección 4, Feature Dsrc/store/process-store.ts— acciones a modificar:updateActivity,updateGateway,updateResource,addResource,deleteResourcesrc/store/simulation-store.ts— estado actual, suscripción a process-storesrc/features/workspace/BpmnCanvas.tsx— canvas actual, sin toolbarmethodology/ANTI_PATTERNS.md— AP.8 (adelanto silencioso)methodology/PATTERNS.md— C.7 (git diff como control preventivo)