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:
307
sprints/sprint-3/ETAPA_4_PROMPT.md
Normal file
307
sprints/sprint-3/ETAPA_4_PROMPT.md
Normal file
@@ -0,0 +1,307 @@
|
||||
# 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`:
|
||||
|
||||
```typescript
|
||||
// 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:
|
||||
|
||||
```typescript
|
||||
interface SimulationState {
|
||||
// ... campos existentes ...
|
||||
lastPersistedExecutedAt: number | null // 🆕 executedAt de la última sim guardada en IndexedDB
|
||||
loadLatestForProcess: (processId: string) => Promise<void> // 🆕
|
||||
}
|
||||
```
|
||||
|
||||
Implementación:
|
||||
|
||||
```typescript
|
||||
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:
|
||||
|
||||
```typescript
|
||||
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`:
|
||||
```typescript
|
||||
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:
|
||||
|
||||
```typescript
|
||||
const { loadLatestForProcess, lastPersistedExecutedAt } = useSimulationStore()
|
||||
|
||||
useEffect(() => {
|
||||
if (currentProcess) {
|
||||
loadLatestForProcess(currentProcess.id)
|
||||
}
|
||||
}, [currentProcess?.id, loadLatestForProcess])
|
||||
```
|
||||
|
||||
**2 — Cómputo del indicador**
|
||||
|
||||
```typescript
|
||||
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:
|
||||
|
||||
```tsx
|
||||
return <div ref={containerRef} className={className} style={{ width: '100%', height: '100%' }} />
|
||||
```
|
||||
|
||||
Cambiar a wrapper con botón superpuesto:
|
||||
|
||||
```tsx
|
||||
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
|
||||
|
||||
```bash
|
||||
# 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)
|
||||
Reference in New Issue
Block a user