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>
126 lines
5.6 KiB
TypeScript
126 lines
5.6 KiB
TypeScript
import Dexie, { type Table } from 'dexie'
|
||
import type { Process, Activity, GatewayConfig, Resource, Simulation, ProcessGroup, AppSettings } from '@/domain/types'
|
||
|
||
export class ProcessCostDb extends Dexie {
|
||
processes!: Table<Process>
|
||
activities!: Table<Activity>
|
||
gateways!: Table<GatewayConfig>
|
||
resources!: Table<Resource>
|
||
simulations!: Table<Simulation>
|
||
groups!: Table<ProcessGroup> // 🆕 Sprint 3
|
||
settings!: Table<AppSettings> // 🆕 Sprint 3
|
||
|
||
constructor() {
|
||
super('ProcessCostPlatform')
|
||
|
||
this.version(1).stores({
|
||
processes: 'id, name, updatedAt',
|
||
activities: 'id, processId, bpmnElementId',
|
||
gateways: 'id, processId, bpmnElementId',
|
||
resources: 'id, processId, name',
|
||
simulations: 'id, processId, executedAt',
|
||
})
|
||
|
||
// Sprint 1: agrega campos de automatización a Activity y volumetría a Process.
|
||
// El upgrade popula datos existentes con defaults para garantizar invariante de tipo.
|
||
this.version(2)
|
||
.stores({
|
||
processes: 'id, name, updatedAt',
|
||
activities: 'id, processId, bpmnElementId',
|
||
gateways: 'id, processId, bpmnElementId',
|
||
resources: 'id, processId, name',
|
||
simulations: 'id, processId, executedAt',
|
||
})
|
||
.upgrade(async (tx) => {
|
||
await tx.table('activities').toCollection().modify((act) => {
|
||
if (act.automatable === undefined) act.automatable = false
|
||
if (act.automatedCostFixed === undefined) act.automatedCostFixed = 0
|
||
if (act.automatedTimeMinutes === undefined) act.automatedTimeMinutes = 0
|
||
})
|
||
await tx.table('processes').toCollection().modify((proc) => {
|
||
if (proc.annualFrequency === undefined) proc.annualFrequency = 1000
|
||
if (proc.analysisHorizonYears === undefined) proc.analysisHorizonYears = 3
|
||
if (proc.automationInvestment === undefined) proc.automationInvestment = 0
|
||
})
|
||
})
|
||
|
||
// Sprint 2: migra ActivityResourceAssignment.utilizationPercent → utilizationMinutes + units.
|
||
// Los stores no cambian — assignments están embebidos en Activity, no son tabla propia.
|
||
this.version(3)
|
||
.stores({
|
||
processes: 'id, name, updatedAt',
|
||
activities: 'id, processId, bpmnElementId',
|
||
gateways: 'id, processId, bpmnElementId',
|
||
resources: 'id, processId, name',
|
||
simulations: 'id, processId, executedAt',
|
||
})
|
||
.upgrade(async (tx) => {
|
||
await tx.table('activities').toCollection().modify((act) => {
|
||
if (!Array.isArray(act.assignedResources)) {
|
||
act.assignedResources = []
|
||
return
|
||
}
|
||
act.assignedResources = act.assignedResources.map((assignment: Record<string, unknown>) => {
|
||
if ('utilizationPercent' in assignment) {
|
||
// Heurística: minutos = round(executionTimeMinutes × utilizationPercent)
|
||
// Si executionTimeMinutes no está disponible o es 0, usar 0 (conservador)
|
||
const execMinutes = typeof act.executionTimeMinutes === 'number' ? act.executionTimeMinutes : 0
|
||
const utilizationMinutes = Math.round(execMinutes * (assignment.utilizationPercent as number))
|
||
return { resourceId: assignment.resourceId, utilizationMinutes, units: 1 }
|
||
}
|
||
if ('utilizationMinutes' in assignment) {
|
||
// Ya está en nuevo formato (idempotencia)
|
||
return {
|
||
resourceId: assignment.resourceId,
|
||
utilizationMinutes: assignment.utilizationMinutes ?? 0,
|
||
units: assignment.units ?? 1,
|
||
}
|
||
}
|
||
// Fallback defensivo
|
||
return { resourceId: assignment.resourceId, utilizationMinutes: 0, units: 1 }
|
||
})
|
||
})
|
||
})
|
||
|
||
// 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' })
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
export const db = new ProcessCostDb()
|