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:
2026-05-27 11:49:43 -03:00
parent d2746f07d1
commit 67e259e6ed
57 changed files with 4372 additions and 82 deletions

View File

@@ -1,5 +1,5 @@
import Dexie, { type Table } from 'dexie'
import type { Process, Activity, GatewayConfig, Resource, Simulation } from '@/domain/types'
import type { Process, Activity, GatewayConfig, Resource, Simulation, ProcessGroup, AppSettings } from '@/domain/types'
export class ProcessCostDb extends Dexie {
processes!: Table<Process>
@@ -7,6 +7,8 @@ export class ProcessCostDb extends Dexie {
gateways!: Table<GatewayConfig>
resources!: Table<Resource>
simulations!: Table<Simulation>
groups!: Table<ProcessGroup> // 🆕 Sprint 3
settings!: Table<AppSettings> // 🆕 Sprint 3
constructor() {
super('ProcessCostPlatform')
@@ -79,6 +81,44 @@ export class ProcessCostDb extends Dexie {
})
})
})
// Sprint 3: agrega tabla de grupos (Process Library), tabla de settings,
// campos groupId/tags en Process, y automatedAssignedResources en Activity.
this.version(4)
.stores({
processes: 'id, name, updatedAt, groupId', // groupId indexado para queries por grupo
activities: 'id, processId, bpmnElementId',
gateways: 'id, processId, bpmnElementId',
resources: 'id, processId, name',
simulations: 'id, processId, executedAt',
groups: 'id, name, updatedAt', // nueva tabla
settings: 'id', // nueva tabla (singleton)
})
.upgrade(async (tx) => {
// Process: agregar groupId (null) y tags ([])
await tx.table('processes').toCollection().modify((proc) => {
if (proc.groupId === undefined) proc.groupId = null
if (!Array.isArray(proc.tags)) proc.tags = []
})
// Activity: agregar automatedAssignedResources ([])
await tx.table('activities').toCollection().modify((act) => {
if (!Array.isArray(act.automatedAssignedResources)) {
act.automatedAssignedResources = []
}
})
// Settings: crear singleton con defaults (put es idempotente por clave primaria)
await tx.table('settings').put({ id: 'singleton', groupLabel: 'Cliente' })
})
// Belt-and-suspenders: garantizar singleton en fresh installs y entornos de test.
// El upgrade también lo crea; este handler cubre el caso en que la tabla sea nueva
// pero el upgrade no haya persistido el put (conocido con fake-indexeddb).
this.on('ready', async () => {
const existing = await this.settings.get('singleton')
if (!existing) {
await this.settings.put({ id: 'singleton', groupLabel: 'Cliente' })
}
})
}
}

View File

@@ -17,7 +17,7 @@
*/
import { db } from './db'
import type { Process, Activity, GatewayConfig, Resource, Simulation } from '@/domain/types'
import type { Process, Activity, GatewayConfig, Resource, Simulation, ProcessGroup, AppSettings } from '@/domain/types'
// ─── Process ─────────────────────────────────────────────────────────────────
@@ -125,13 +125,56 @@ export const simulationRepo = {
},
async getLatestByProcess(processId: string): Promise<Simulation | undefined> {
return db.simulations
// Ordena por executedAt descendente para retornar la simulación más reciente.
// No se usa .last() porque el primary key es UUID (no ordenado cronológicamente).
const all = await db.simulations
.where('processId')
.equals(processId)
.last()
.toArray()
if (!all.length) return undefined
return all.reduce((latest, sim) => sim.executedAt > latest.executedAt ? sim : latest)
},
async getByProcess(processId: string): Promise<Simulation[]> {
return db.simulations.where('processId').equals(processId).toArray()
},
}
// ─── 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)
},
}