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>
16 KiB
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
groupsysettingsdisponibles.Process.groupId?yProcess.tags?existen como campos opcionales.Activity.automatedAssignedResources?existe como campo opcional. - La app tiene:
ImportPageen/(home actual),WorkspacePageen/workspace/$processId,ReportPageen/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
- 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. - Crear
library-store.ts: Zustand store para operaciones de biblioteca (grupos + lista de procesos). - Extender
repositories.tscongroupRepoysettingsRepo. - Actualizar
router.tsx: LibraryPage en/, ImportPage en/import. - Actualizar
ImportPagey todos los archivos de tests que construyenProcessoActivityliteralmente para agregar los nuevos campos requeridos. - Promover
Process.groupIdyProcess.tagsa campos requeridos entypes.ts(quitar?).Activity.automatedAssignedResourcespermanece 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
updatedAtmá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 elgroupLabelconfigurado), 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=XXXXpara 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-branchsobre 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:
-
Con escenario automatizado simulado (hay
resultAutomateden la última simulación): badge verde — fondo#f0fdf4, borde#86efac, texto#15803d, íconoti-trending-up, valor "USD X.XXX". Label debajo: "Ahorro anual proyectado" (10px, color tertiary). -
Simulado solo actual (hay
resultpero noresultAutomated): badge neutro — fondobackground-secondary, borde tertiary, texto secondary, íconoti-chart-bar, valor "USD X.XXX". Label debajo: "Costo anual actual" (10px, color tertiary). El costo anual =result.totalCost × annualFrequencydel proceso. -
Sin simular: texto "Sin simular" (11px, italic, color tertiary). Sin badge.
Empty states
Home sin grupos ni procesos (primera vez):
- Ícono grande
ti-buildingo 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
?degroupIdytagsenProcess— campos requeridos. automatedAssignedResourcespermanece opcional (?) enActivity— se vuelve requerido en Etapa 5.
src/persistence/repositories.ts
Agregar al final, siguiendo el patrón existente:
// ─── 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:
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 estadoview: 'home' | 'group'yactiveGroupId: string | nullcon React state.- Sub-componentes inline o en archivos separados según criterio del agente (Nivel 1 — decisión libre).
src/router.tsx
// 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:
- Agregar
groupId: null, tags: []al objetoProcessque se construye al importar. - Leer el search param
groupIdde la URL: si está presente, usarlo en lugar denull. UsaruseSearchde TanStack Router.
// 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
groupLabelen 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.tsosimulation-store.ts Activity.automatedAssignedResourcessigue 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
# 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.tssrc/persistence/repositories.tssrc/store/library-store.ts(nuevo)src/features/library/(nuevos)src/router.tsxsrc/features/import/ImportPage.tsxtests/**(solo los que tienen objetosProcessliterales que necesitangroupId/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
groupIda ImportPage: el proceso queda asignado al grupo. Process.groupIdyProcess.tagsson 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
- Resultado de
git diff --name-only HEADcon la lista de archivos modificados. - Cantidad de archivos de tests actualizados con defaults
groupId/tags. - Resultado de
npm run buildynpm run test. - Cualquier decisión de iniciativa tomada (Nivel 1/2/3).
- 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 seguirsrc/persistence/repositories.ts— patrón de repos a seguirsrc/router.tsx— estado actual del routingsrc/features/import/ImportPage.tsx— construcción actual de objetos Processmethodology/ANTI_PATTERNS.md— AP.9 (restricciones contradictorias)methodology/PATTERNS.md— C.7 (git diff como control preventivo)