Files
inq-roi-simulador-web/sprints/sprint-3/ETAPA_4_PROMPT.md
Marcos Benítez 67e259e6ed 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>
2026-05-27 11:49:43 -03:00

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. SettingsPage disponible.
  • process-store.ts expone: updateActivity, updateGateway, updateResource, addResource, deleteResource, updateProcess. Ninguna de estas acciones actualiza process.updatedAt excepto updateProcess.
  • simulation-store.ts tiene lastSimulatedAt: Date | null (in-memory, se pierde al recargar) y persistResults (guarda en IndexedDB con executedAt: Date.now()).
  • BpmnCanvas.tsx: viewer bpmn-js puro, sin toolbar, retorna un <div> que es el contenedor del canvas. Ya llama canvas.zoom('fit-viewport', 'auto') al importar el XML.

Esta etapa agrega dos features independientes:

  1. Indicador stale — aviso persistente cuando el proceso tiene cambios sin simular.
  2. 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

  1. process-store.ts — bump process.updatedAt en mutaciones de actividades/gateways/recursos.
  2. simulation-store.ts — agregar lastPersistedExecutedAt: number | null (cargado desde IndexedDB, sobrevive recargas) y la acción loadLatestForProcess(processId).
  3. WorkspacePage — orquesta: llama loadLatestForProcess cuando el proceso está cargado, y computa isStale comparando 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, ícono AlertCircle (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 (porque persistResults actualiza lastPersistedExecutedAt).

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.ts
  • src/store/simulation-store.ts
  • src/features/workspace/WorkspacePage.tsx
  • src/features/workspace/BpmnCanvas.tsx
  • src/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:

  1. Stale persiste en recarga: modificar una actividad → recargar página → el indicador sigue visible → simular → desaparece.
  2. Sin sim previa: proceso recién importado sin simular nunca → indicador visible (ya que updatedAt > 0 > null ?? 0).
  3. Fit-viewport: abrir un diagrama complejo → click en el botón → el canvas se encuadra.
  4. Sin falsos positivos: proceso recién simulado sin cambios posteriores → indicador ausente.

Criterios de aceptación

  • Modificar actividad/recurso/gateway actualiza process.updatedAt en 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

  1. Resultado de git diff --name-only HEAD.
  2. Resultado de npm run build y npm run test.
  3. Confirmación de la verificación funcional (4 casos).
  4. Si se implementó el banner en ReportPage (wow): declararlo.
  5. Cualquier decisión de iniciativa tomada (Nivel 1/2/3).
  6. Si hubo archivo fuera del scope: declararlo con justificación.

Referencias

  • sprints/sprint-3/BRIEF.md — sección 4, Feature D
  • src/store/process-store.ts — acciones a modificar: updateActivity, updateGateway, updateResource, addResource, deleteResource
  • src/store/simulation-store.ts — estado actual, suscripción a process-store
  • src/features/workspace/BpmnCanvas.tsx — canvas actual, sin toolbar
  • methodology/ANTI_PATTERNS.md — AP.8 (adelanto silencioso)
  • methodology/PATTERNS.md — C.7 (git diff como control preventivo)