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 activities!: Table gateways!: Table resources!: Table simulations!: Table groups!: Table // 🆕 Sprint 3 settings!: Table // 🆕 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) => { 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()