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>
339 lines
16 KiB
Markdown
339 lines
16 KiB
Markdown
# Sprint 3 — Etapa 2: Vista de biblioteca + gestión de grupos
|
||
|
||
## 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:**
|
||
- Etapa 1 completada: Dexie v4 en producción. Tablas `groups` y `settings` disponibles. `Process.groupId?` y `Process.tags?` existen como campos opcionales. `Activity.automatedAssignedResources?` existe como campo opcional.
|
||
- La app tiene: `ImportPage` en `/` (home actual), `WorkspacePage` en `/workspace/$processId`, `ReportPage` en `/report/$processId`.
|
||
- Stack de routing: TanStack Router. Store: Zustand (`process-store.ts`, `simulation-store.ts`). Repos: `repositories.ts`.
|
||
|
||
**Esta etapa construye la Process Library y la convierte en la home de la app.**
|
||
|
||
---
|
||
|
||
## Objetivo
|
||
|
||
1. Crear `LibraryPage`: la nueva home de la aplicación — lista de grupos, navegación a procesos dentro de un grupo, búsqueda, tags, CRUD de grupos.
|
||
2. Crear `library-store.ts`: Zustand store para operaciones de biblioteca (grupos + lista de procesos).
|
||
3. Extender `repositories.ts` con `groupRepo` y `settingsRepo`.
|
||
4. Actualizar `router.tsx`: LibraryPage en `/`, ImportPage en `/import`.
|
||
5. Actualizar `ImportPage` y todos los archivos de tests que construyen `Process` o `Activity` literalmente para agregar los nuevos campos requeridos.
|
||
6. Promover `Process.groupId` y `Process.tags` a campos **requeridos** en `types.ts` (quitar `?`). `Activity.automatedAssignedResources` permanece opcional hasta Etapa 5.
|
||
|
||
---
|
||
|
||
## Contrato visual — respetar sin interpretar
|
||
|
||
El diseño fue aprobado por el director. Replicarlo fielmente. No agregar features visuales que no estén acá.
|
||
|
||
### Vista home (lista de grupos)
|
||
|
||
**Header:**
|
||
- Título "Biblioteca de procesos" (18px, weight 500) con subtítulo "N procesos · N clientes" (12px, color tertiary).
|
||
- Botón naranja `#F59845` "Importar BPMN" (ícono upload) alineado a la derecha. Al hacer click navega a `/import`.
|
||
- Sin botón de configuración en el header.
|
||
|
||
**Barra de búsqueda:**
|
||
- Input full-width con ícono de lupa a la izquierda. Placeholder "Buscar proceso por nombre...". Filtra en tiempo real sobre todos los procesos de todos los grupos (búsqueda global).
|
||
- Cuando hay búsqueda activa, mostrar resultados flat (sin agrupar por grupo) con chip que indica a qué grupo pertenece cada proceso.
|
||
|
||
**Fila de tags:**
|
||
- Label "Tags:" seguido de chips seleccionables (border redondeado, fondo blanco, borde tertiary).
|
||
- Estado activo: fondo `#fff3e6`, borde `#F59845`, texto `#7a3e00`.
|
||
- Múltiples tags activos filtran en modo AND (proceso debe tener TODOS los tags seleccionados).
|
||
- Los tags mostrados son los que existen en al menos un proceso. Si no hay tags: no mostrar la fila.
|
||
|
||
**Sección "Clientes" (o el `groupLabel` configurado):**
|
||
- Label de sección en uppercase, 12px, color secondary, con contador de grupos como badge gris pequeño.
|
||
- Grid de group cards: `repeat(auto-fill, minmax(160px, 1fr))`, gap 10px.
|
||
|
||
**Group card (existente):**
|
||
- Fondo `background-primary`, borde 0.5px tertiary, border-radius-lg, borde izquierdo 3px `#F59845`.
|
||
- Número de procesos: 24px, weight 500, color `#F59845`, arriba.
|
||
- Nombre del grupo: 13px, weight 500, debajo del número, truncado con ellipsis.
|
||
- Fecha: "Actualizado hace X días" (11px, color tertiary), calculada desde el `updatedAt` más reciente entre los procesos del grupo.
|
||
- Al hacer click: navega a la vista de grupo (estado interno, sin cambio de URL).
|
||
- Al hover: borde cambia a `border-secondary`.
|
||
|
||
**Group card "Nuevo cliente" (estado normal):**
|
||
- Mismo tamaño que las otras cards. Borde dashed 0.5px tertiary. Fondo `background-primary`.
|
||
- Contenido centrado: ícono `ti-plus` + texto "Nuevo cliente" (o el `groupLabel` configurado), color tertiary.
|
||
- Al hover: borde `#F59845`, texto `#F59845`.
|
||
- Al hacer click: **transforma inline en modo edición** (sin abrir modal).
|
||
|
||
**Group card "Nuevo cliente" (estado edición):**
|
||
- La misma card cambia: borde 1.5px `#F59845`, muestra input de texto + botones "Crear" (naranja) y "Cancelar" (ghost).
|
||
- Input con placeholder "Nombre del cliente...".
|
||
- Enter confirma. Escape cancela. Nombre vacío → cancelar sin crear.
|
||
- Al confirmar: la card de input se reemplaza por la nueva group card en la grilla, y aparece una nueva card "Nuevo cliente" al final.
|
||
|
||
**Sección "Sin clasificar":**
|
||
- Fila full-width, borde dashed, separada de la grilla de grupos.
|
||
- Ícono `ti-folder-off`, texto "Procesos sin cliente asignado" (13px, weight 500, color secondary), contador (11px, color tertiary).
|
||
- Solo visible si hay al menos un proceso con `groupId === null`.
|
||
- Al hacer click: navega a vista de grupo con `groupId === null`.
|
||
|
||
### Vista de grupo (detalle)
|
||
|
||
**Header:**
|
||
- Breadcrumb: link "← Biblioteca" (color `#F59845`, cursor pointer) + separador "/" + nombre del grupo (color primary, weight 500).
|
||
- Subtítulo: "N proceso(s)" (12px, color tertiary).
|
||
- Botón naranja "Importar BPMN" a la derecha. Al hacer click: navega a `/import?groupId=XXXX` para que el proceso importado quede asignado a este grupo automáticamente.
|
||
- Al hacer click en "← Biblioteca": vuelve a la vista home.
|
||
|
||
**Barra de búsqueda:**
|
||
- Input con placeholder "Buscar en este cliente...". Filtra los procesos del grupo en tiempo real.
|
||
|
||
**Lista de process cards:**
|
||
- Una card por proceso, apiladas verticalmente.
|
||
|
||
**Process card:**
|
||
- Ícono `ti-git-branch` sobre fondo `#fff3e6` (36×36px, border-radius-md), a la izquierda.
|
||
- Columna central: nombre del proceso (13px weight 500, truncado), debajo "Modificado hace X días" (ícono `ti-clock` + texto, 11px, color tertiary), debajo chips de tags (11px, bg secondary, borde tertiary, border-radius 10px).
|
||
- Columna derecha (min-width 90px, text-align right): badge de KPI + label.
|
||
|
||
**KPI badge — tres estados:**
|
||
|
||
1. **Con escenario automatizado simulado** (hay `resultAutomated` en la última simulación): badge verde — fondo `#f0fdf4`, borde `#86efac`, texto `#15803d`, ícono `ti-trending-up`, valor "USD X.XXX". Label debajo: "Ahorro anual proyectado" (10px, color tertiary).
|
||
|
||
2. **Simulado solo actual** (hay `result` pero no `resultAutomated`): badge neutro — fondo `background-secondary`, borde tertiary, texto secondary, ícono `ti-chart-bar`, valor "USD X.XXX". Label debajo: "Costo anual actual" (10px, color tertiary). El costo anual = `result.totalCost × annualFrequency` del proceso.
|
||
|
||
3. **Sin simular**: texto "Sin simular" (11px, italic, color tertiary). Sin badge.
|
||
|
||
### Empty states
|
||
|
||
**Home sin grupos ni procesos (primera vez):**
|
||
- Ícono grande `ti-building` o similar centrado, texto "Tu biblioteca está vacía" (16px), subtítulo "Importá tu primer BPMN para empezar" (13px, color secondary), botón naranja "Importar BPMN".
|
||
|
||
**Grupo sin procesos:**
|
||
- Ícono `ti-file-off`, texto "No hay procesos en este cliente.", subtítulo "Importá un BPMN para comenzar." (13px, color secondary).
|
||
|
||
---
|
||
|
||
## Cambios autorizados y arquitectura
|
||
|
||
### `src/domain/types.ts`
|
||
- Quitar `?` de `groupId` y `tags` en `Process` — campos requeridos.
|
||
- `automatedAssignedResources` permanece opcional (`?`) en `Activity` — se vuelve requerido en Etapa 5.
|
||
|
||
### `src/persistence/repositories.ts`
|
||
Agregar al final, siguiendo el patrón existente:
|
||
|
||
```typescript
|
||
// ─── ProcessGroup ─────────────────────────────────────────────────────────────
|
||
|
||
/** @internal Solo llamar desde src/store/library-store.ts */
|
||
export const groupRepo = {
|
||
async save(group: ProcessGroup): Promise<void> {
|
||
await db.groups.put(group)
|
||
},
|
||
async getAll(): Promise<ProcessGroup[]> {
|
||
return db.groups.orderBy('name').toArray()
|
||
},
|
||
async getById(id: string): Promise<ProcessGroup | undefined> {
|
||
return db.groups.get(id)
|
||
},
|
||
async delete(id: string): Promise<void> {
|
||
// Procesos del grupo pasan a "Sin clasificar" (groupId = null)
|
||
await db.transaction('rw', [db.groups, db.processes], async () => {
|
||
await db.processes.where('groupId').equals(id).modify({ groupId: null })
|
||
await db.groups.delete(id)
|
||
})
|
||
},
|
||
}
|
||
|
||
// ─── AppSettings ──────────────────────────────────────────────────────────────
|
||
|
||
/** @internal Solo llamar desde src/store/library-store.ts */
|
||
export const settingsRepo = {
|
||
async get(): Promise<AppSettings> {
|
||
const s = await db.settings.get('singleton')
|
||
return s ?? { id: 'singleton', groupLabel: 'Cliente' }
|
||
},
|
||
async update(updates: Partial<Omit<AppSettings, 'id'>>): Promise<void> {
|
||
await db.settings.update('singleton', updates)
|
||
},
|
||
}
|
||
```
|
||
|
||
Actualizar el import en la cabecera para incluir `ProcessGroup` y `AppSettings`.
|
||
|
||
### `src/store/library-store.ts` (NUEVO)
|
||
|
||
Zustand store con la siguiente interfaz:
|
||
|
||
```typescript
|
||
interface LibraryState {
|
||
groups: ProcessGroup[]
|
||
processes: Process[] // todos los procesos, para búsqueda global
|
||
settings: AppSettings
|
||
isLoading: boolean
|
||
|
||
// Inicialización
|
||
load: () => Promise<void>
|
||
|
||
// Grupos
|
||
createGroup: (name: string) => Promise<ProcessGroup>
|
||
deleteGroup: (id: string) => Promise<void>
|
||
|
||
// Procesos
|
||
assignProcessToGroup: (processId: string, groupId: string | null) => Promise<void>
|
||
|
||
// Computed helpers (derivados del estado, no acciones async)
|
||
getProcessesByGroup: (groupId: string | null) => Process[]
|
||
getLatestSimulation: (processId: string) => Promise<Simulation | undefined>
|
||
}
|
||
```
|
||
|
||
`load()` carga grupos, todos los procesos, y settings en paralelo.
|
||
`getProcessesByGroup(null)` retorna los procesos con `groupId === null` ("Sin clasificar").
|
||
`getLatestSimulation` usa `simulationRepo.getLatestByProcess` — agregar `simulationRepo` al import del store.
|
||
|
||
Respetar la regla arquitectónica: el store importa desde `repositories`, no directamente desde `db`.
|
||
|
||
### `src/features/library/` (NUEVO directorio)
|
||
|
||
Crear mínimamente:
|
||
- `LibraryPage.tsx` — componente principal, gestiona el estado `view: 'home' | 'group'` y `activeGroupId: string | null` con React state.
|
||
- Sub-componentes inline o en archivos separados según criterio del agente (Nivel 1 — decisión libre).
|
||
|
||
### `src/router.tsx`
|
||
```typescript
|
||
// Antes:
|
||
// indexRoute → path: '/', component: ImportPage
|
||
|
||
// Después:
|
||
const indexRoute = createRoute({
|
||
getParentRoute: () => rootRoute,
|
||
path: '/',
|
||
component: LibraryPage, // nuevo home
|
||
})
|
||
|
||
const importRoute = createRoute({
|
||
getParentRoute: () => rootRoute,
|
||
path: '/import',
|
||
component: ImportPage, // nueva ruta
|
||
})
|
||
|
||
// Agregar importRoute al routeTree
|
||
```
|
||
|
||
### `src/features/import/ImportPage.tsx`
|
||
|
||
Dos cambios:
|
||
1. Agregar `groupId: null, tags: []` al objeto `Process` que se construye al importar.
|
||
2. Leer el search param `groupId` de la URL: si está presente, usarlo en lugar de `null`. Usar `useSearch` de TanStack Router.
|
||
|
||
```typescript
|
||
// Leer groupId del search param (puede no estar presente)
|
||
const search = useSearch({ from: '/import' })
|
||
const targetGroupId = (search as { groupId?: string }).groupId ?? null
|
||
|
||
// Al construir el Process:
|
||
const process: Process = {
|
||
// ... campos existentes ...
|
||
groupId: targetGroupId,
|
||
tags: [],
|
||
}
|
||
```
|
||
|
||
Después de importar con éxito, en lugar de navegar a workspace: navegar de vuelta a `/` si el usuario vino sin groupId, o puede navegar directamente al workspace (comportamiento actual — mantener).
|
||
|
||
### Archivos de tests con objetos `Process` o `Activity` literales
|
||
|
||
Buscar todos los archivos bajo `tests/` que construyan objetos `{ id: ..., name: ..., bpmnXml: ... }` como `Process` o similar. Agregar `groupId: null, tags: []` a cada objeto `Process`. No agregar `automatedAssignedResources` todavía (campo sigue siendo opcional).
|
||
|
||
Este es un scope amplio autorizado: **cualquier archivo de tests que falle compilación TypeScript por `Process.groupId` o `Process.tags` faltantes está autorizado a ser modificado para agregar esos campos con sus defaults**.
|
||
|
||
---
|
||
|
||
## ❌ Lo que NO entra en esta etapa
|
||
|
||
- Renombrar grupos (edición del nombre post-creación) — Sprint 3 o 4
|
||
- Configuración de `groupLabel` en UI — Etapa 3
|
||
- Tags en procesos (chip input en workspace) — Etapa 3
|
||
- Drag & drop de procesos entre grupos — backlog
|
||
- Eliminar procesos desde la biblioteca — backlog
|
||
- Cambios en `WorkspacePage`, `ReportPage`, `ActivityPanel`, o cualquier feature de workspace
|
||
- Cambios en el motor de simulación o en exports PDF/CSV
|
||
- Cambios en `process-store.ts` o `simulation-store.ts`
|
||
- `Activity.automatedAssignedResources` sigue siendo opcional — no promover a requerido
|
||
|
||
Si agregar `groupId`/`tags` como requeridos en `types.ts` genera errores TypeScript en archivos fuera de los autorizados: listar los archivos afectados y reportar antes de modificarlos. No resolver silenciosamente. Ver AP.9.
|
||
|
||
---
|
||
|
||
## 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 archivos de:
|
||
- `src/domain/types.ts`
|
||
- `src/persistence/repositories.ts`
|
||
- `src/store/library-store.ts` (nuevo)
|
||
- `src/features/library/` (nuevos)
|
||
- `src/router.tsx`
|
||
- `src/features/import/ImportPage.tsx`
|
||
- `tests/**` (solo los que tienen objetos `Process` literales que necesitan `groupId`/`tags`)
|
||
|
||
Si aparece cualquier archivo de `src/features/workspace/`, `src/features/report/`, `src/store/process-store.ts`, o `src/lib/export/`: revertir y reportar.
|
||
|
||
---
|
||
|
||
## Criterios de aceptación
|
||
|
||
- [ ] LibraryPage es la home (`/`). ImportPage está en `/import`.
|
||
- [ ] Grupos se listan en grid con conteo, nombre y fecha de última modificación.
|
||
- [ ] "Nuevo cliente" crea un grupo inline (sin modal): click → input → Enter confirma / Escape cancela.
|
||
- [ ] "Sin clasificar" muestra procesos sin grupo. Oculto si no hay ninguno.
|
||
- [ ] Navegación home ↔ grupo funciona con breadcrumb.
|
||
- [ ] KPI badge en process card muestra: ahorro proyectado (verde), costo actual (neutro), o "Sin simular".
|
||
- [ ] Búsqueda en home filtra por nombre de proceso en tiempo real.
|
||
- [ ] Filtro por tags en modo AND funciona.
|
||
- [ ] "Importar BPMN" desde dentro de un grupo pasa `groupId` a ImportPage: el proceso queda asignado al grupo.
|
||
- [ ] `Process.groupId` y `Process.tags` son campos requeridos. Build sin errores TypeScript.
|
||
- [ ] Todos los tests previos siguen verdes.
|
||
- [ ] Validación visual del director aprobada.
|
||
|
||
---
|
||
|
||
## Criterio de wow
|
||
|
||
Los empty states y las transiciones visuales son la diferencia entre una herramienta que se siente terminada y una que se siente en construcción. Implementar:
|
||
- Empty state en home (primera vez sin grupos) con CTA prominente.
|
||
- Empty state en grupo vacío con mensaje contextual.
|
||
- Transición suave (CSS transition opacity/transform 150ms) al entrar y salir de un grupo.
|
||
|
||
---
|
||
|
||
## Reporte esperado al terminar
|
||
|
||
1. Resultado de `git diff --name-only HEAD` con la lista de archivos modificados.
|
||
2. Cantidad de archivos de tests actualizados con defaults `groupId`/`tags`.
|
||
3. Resultado de `npm run build` y `npm run test`.
|
||
4. Cualquier decisión de iniciativa tomada (Nivel 1/2/3).
|
||
5. Si hubo algún archivo fuera del scope que requirió modificación: declararlo explícitamente con justificación.
|
||
|
||
---
|
||
|
||
## Referencias
|
||
|
||
- `sprints/sprint-3/BRIEF.md` — sección 4 (Feature B)
|
||
- `src/store/process-store.ts` — patrón de Zustand a seguir
|
||
- `src/persistence/repositories.ts` — patrón de repos a seguir
|
||
- `src/router.tsx` — estado actual del routing
|
||
- `src/features/import/ImportPage.tsx` — construcción actual de objetos Process
|
||
- `methodology/ANTI_PATTERNS.md` — AP.9 (restricciones contradictorias)
|
||
- `methodology/PATTERNS.md` — C.7 (git diff como control preventivo)
|