15 KiB
Sprint 4 — Etapa 3: Migración de actividades, gateways y recursos globales
Contexto
InQ ROI — React 18 + Vite + TypeScript + TanStack Router + Zustand + shadcn/ui + Tailwind. Etapas 1 y 2 completadas: auth con Google, rutas protegidas, procesos/grupos/settings en Supabase. Esta etapa migra actividades, gateways y recursos de Dexie a Supabase. El cambio más significativo: recursos pasan de ser por-proceso a un catálogo global del equipo.
Al terminar esta etapa, Dexie solo retiene simulaciones (Etapa 4).
Prerequisitos — verificar antes de empezar
- Scripts 001-004 ejecutados en Supabase (de Etapas 1 y 2)
src/auth/AuthContext.tsxconuseAuth()funcionandosrc/persistence/supabase/con process-repo, group-repo, settings-repo
Alcance estricto — qué entra en esta etapa
- Scripts SQL para
resources,activities,activity_resource_assignments,gateways - Nuevos repos Supabase para las cuatro entidades
- Actualizar
process-store.tspara usar repos Supabase - Actualizar
useReportData.tspara cargar recursos desde Supabase - Actualizar
ResourcesPanel.tsxmínimamente: crear recursos sinprocessId - Actualizar
process-repo.tsdelete: eliminar limpieza Dexie de activities/gateways/resources (pasan a CASCADE en Supabase); conservar limpieza Dexie solo para simulaciones - Modificar tipo
Resourceensrc/domain/types.ts: hacerprocessIdopcional (processId?: UUID)
Qué NO entra en esta etapa
- Migración de simulaciones → Etapa 4
- Eliminar Dexie del proyecto → Etapa 4
- UX de catálogo global de recursos (selector, panel dedicado) → Etapa 7
- Tabla
user_groups,process_access→ Etapa 5 - Avatar / "Actualizado por" en UI → Etapa 6
- Cambiar ningún otro tipo en
src/domain/types.ts
Scripts SQL — crear en supabase/migrations/
005_create_resources.sql
-- Catálogo global de recursos — sin process_id
CREATE TABLE IF NOT EXISTS public.resources (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
name text NOT NULL,
type text NOT NULL CHECK (type IN ('role','person','system','equipment','supply')),
cost_per_hour numeric NOT NULL DEFAULT 0,
created_by uuid REFERENCES public.users(id),
updated_by uuid REFERENCES public.users(id),
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
ALTER TABLE public.resources ENABLE ROW LEVEL SECURITY;
-- Todos los autenticados pueden leer y escribir recursos (catálogo compartido)
CREATE POLICY "all_resources" ON public.resources
FOR ALL USING (auth.uid() IS NOT NULL)
WITH CHECK (auth.uid() IS NOT NULL);
006_create_activities.sql
CREATE TABLE IF NOT EXISTS public.activities (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
process_id uuid NOT NULL REFERENCES public.processes(id) ON DELETE CASCADE,
bpmn_element_id text NOT NULL,
name text NOT NULL DEFAULT '',
type text NOT NULL DEFAULT 'task' CHECK (type IN ('task','subprocess')),
direct_cost_fixed numeric NOT NULL DEFAULT 0,
execution_time_minutes numeric NOT NULL DEFAULT 0,
automatable boolean NOT NULL DEFAULT false,
automated_cost_fixed numeric NOT NULL DEFAULT 0,
automated_time_minutes numeric NOT NULL DEFAULT 0,
created_by uuid REFERENCES public.users(id),
updated_by uuid REFERENCES public.users(id),
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
CREATE UNIQUE INDEX IF NOT EXISTS activities_process_bpmn_idx
ON public.activities(process_id, bpmn_element_id);
ALTER TABLE public.activities ENABLE ROW LEVEL SECURITY;
CREATE POLICY "all_activities" ON public.activities
FOR ALL USING (auth.uid() IS NOT NULL)
WITH CHECK (auth.uid() IS NOT NULL);
007_create_activity_resource_assignments.sql
-- Reemplaza los arrays embebidos assignedResources / automatedAssignedResources
CREATE TABLE IF NOT EXISTS public.activity_resource_assignments (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
activity_id uuid NOT NULL REFERENCES public.activities(id) ON DELETE CASCADE,
resource_id uuid NOT NULL REFERENCES public.resources(id) ON DELETE CASCADE,
utilization_minutes numeric NOT NULL DEFAULT 0,
units numeric NOT NULL DEFAULT 1,
scenario text NOT NULL DEFAULT 'actual' CHECK (scenario IN ('actual','automated'))
);
ALTER TABLE public.activity_resource_assignments ENABLE ROW LEVEL SECURITY;
CREATE POLICY "all_assignments" ON public.activity_resource_assignments
FOR ALL USING (auth.uid() IS NOT NULL)
WITH CHECK (auth.uid() IS NOT NULL);
008_create_gateways.sql
CREATE TABLE IF NOT EXISTS public.gateways (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
process_id uuid NOT NULL REFERENCES public.processes(id) ON DELETE CASCADE,
bpmn_element_id text NOT NULL,
gateway_type text NOT NULL CHECK (gateway_type IN ('exclusive','parallel','inclusive','event-based')),
branches jsonb NOT NULL DEFAULT '[]',
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
CREATE UNIQUE INDEX IF NOT EXISTS gateways_process_bpmn_idx
ON public.gateways(process_id, bpmn_element_id);
ALTER TABLE public.gateways ENABLE ROW LEVEL SECURITY;
CREATE POLICY "all_gateways" ON public.gateways
FOR ALL USING (auth.uid() IS NOT NULL)
WITH CHECK (auth.uid() IS NOT NULL);
Ejecutar en orden (005 → 006 → 007 → 008) en Supabase SQL Editor antes de continuar. Parar si alguno falla.
Modificación de tipo TypeScript — src/domain/types.ts
Hacer processId opcional en la interfaz Resource:
// ANTES:
export interface Resource {
id: UUID
processId: UUID // ← campo requerido
name: string
type: ResourceType
costPerHour: number
}
// DESPUÉS:
export interface Resource {
id: UUID
processId?: UUID // ← opcional (backward compat con tests Dexie existentes)
name: string
type: ResourceType
costPerHour: number
}
Solo este cambio en types.ts. No modificar ninguna otra interfaz.
Nuevos repositorios — src/persistence/supabase/
resource-repo.ts
Catálogo global — no filtra por proceso.
export const supabaseResourceRepo = {
async save(resource: Resource, userId: string): Promise<void>
// upsert: { id, name, type, cost_per_hour, updated_by: userId, updated_at: now() }
// en insert: también created_by: userId
// NO escribir processId en la DB
async getAll(): Promise<Resource[]>
// SELECT * FROM resources ORDER BY name
// from_db: { id, name, type: row.type, costPerHour: row.cost_per_hour }
// processId NO se popula en el objeto retornado (queda undefined)
async delete(resourceId: string): Promise<void>
// DELETE FROM resources WHERE id = resourceId
}
activity-repo.ts
Las actividades tienen asignaciones embebidas en el tipo TypeScript pero normalizadas en la DB.
export const supabaseActivityRepo = {
async save(activity: Activity, userId: string): Promise<void>
// 1. Upsert en activities (sin los arrays de recursos)
// 2. DELETE FROM activity_resource_assignments WHERE activity_id = activity.id
// 3. INSERT assignments de activity.assignedResources (scenario='actual')
// 4. INSERT assignments de activity.automatedAssignedResources (scenario='automated')
async saveMany(activities: Activity[], userId: string): Promise<void>
// Loop de save() — performance no crítica para import
async getByProcess(processId: string): Promise<Activity[]>
// SELECT activities.*, activity_resource_assignments(*) WHERE process_id = processId
// Reconstruir Activity con:
// assignedResources: assignments.filter(a => a.scenario === 'actual').map(toAssignment)
// automatedAssignedResources: assignments.filter(a => a.scenario === 'automated').map(toAssignment)
async getByBpmnElementId(processId: string, bpmnElementId: string): Promise<Activity | undefined>
// SELECT con WHERE process_id = ? AND bpmn_element_id = ? + join assignments
async deleteByProcess(processId: string): Promise<void>
// DELETE WHERE process_id = processId (assignments se eliminan por CASCADE)
}
Mapeo Activity TypeScript ↔ PostgreSQL:
| TypeScript | PostgreSQL |
|---|---|
id |
id |
processId |
process_id |
bpmnElementId |
bpmn_element_id |
name |
name |
type |
type |
directCostFixed |
direct_cost_fixed |
executionTimeMinutes |
execution_time_minutes |
automatable |
automatable |
automatedCostFixed |
automated_cost_fixed |
automatedTimeMinutes |
automated_time_minutes |
assignedResources |
via activity_resource_assignments WHERE scenario='actual' |
automatedAssignedResources |
via activity_resource_assignments WHERE scenario='automated' |
gateway-repo.ts
export const supabaseGatewayRepo = {
async save(gateway: GatewayConfig): Promise<void>
// upsert: { id, process_id, bpmn_element_id, gateway_type, branches: JSON.stringify(gateway.branches) }
async saveMany(gateways: GatewayConfig[]): Promise<void>
// Loop de save()
async getByProcess(processId: string): Promise<GatewayConfig[]>
// SELECT WHERE process_id = processId
// from_db: branches: JSON.parse(row.branches) as GatewayBranch[]
async deleteByProcess(processId: string): Promise<void>
// DELETE WHERE process_id (CASCADE ya lo maneja, pero mantener para explicitez)
}
Mapeo GatewayConfig:
| TypeScript | PostgreSQL |
|---|---|
id |
id |
processId |
process_id |
bpmnElementId |
bpmn_element_id |
gatewayType |
gateway_type |
branches (array) |
branches (jsonb) — JSON.stringify / JSON.parse |
Actualización de stores y componentes
src/store/process-store.ts
Reemplazar todas las llamadas a repos Dexie con los nuevos repos Supabase.
loadProcess: cambiar resourceRepo.getByProcess(processId) → supabaseResourceRepo.getAll()
Los recursos se cargan globalmente. El store los tiene todos disponibles para el workspace.
setCurrentProcess: igual — cargar recursos con getAll()
updateActivity: activityRepo.save(activity) → supabaseActivityRepo.save(activity, userId)
updateGateway: gatewayRepo.save(gateway) → supabaseGatewayRepo.save(gateway)
(gateways no tienen userId — no hay created_by/updated_by en el schema de gateways)
addResource / updateResource: resourceRepo.save(resource) → supabaseResourceRepo.save(resource, userId)
Al crear un recurso, NO incluir processId en el objeto que se pasa al repo.
deleteResource: resourceRepo.delete(resourceId) → supabaseResourceRepo.delete(resourceId)
src/persistence/supabase/process-repo.ts — actualizar delete
Después de Etapa 3, activities/gateways/resources tienen CASCADE en Supabase.
Actualizar el método delete para que limpie Dexie solo de simulaciones:
async delete(id: string): Promise<void> {
// Supabase CASCADE elimina automáticamente: activities, gateways, activity_resource_assignments
await supabase.from('processes').delete().eq('id', id)
// TODO Sprint 4 Etapa 4: eliminar limpieza Dexie cuando simulaciones migren a Supabase
await db.transaction('rw', [db.simulations], async () => {
await db.simulations.where('processId').equals(id).delete()
})
// Nota: activities, gateways, resources ya NO están en Dexie — no limpiar
}
src/features/report/useReportData.ts
Cambiar resourceRepo.getByProcess(processId) → supabaseResourceRepo.getAll()
src/features/workspace/ResourcesPanel.tsx
Al crear un nuevo recurso, no incluir processId en el objeto:
// ANTES:
const newResource: Resource = {
id: crypto.randomUUID(),
processId: currentProcess.id, // ← eliminar esta línea
name: ...,
type: ...,
costPerHour: ...,
}
// DESPUÉS:
const newResource: Resource = {
id: crypto.randomUUID(),
name: ...,
type: ...,
costPerHour: ...,
}
No hacer otros cambios de UX en ResourcesPanel — el rediseño completo (selección desde catálogo) es Etapa 7.
src/features/import/ImportPage.tsx
Si usa activityRepo.saveMany() o gatewayRepo.saveMany(), actualizar a los repos Supabase.
Asegurarse de que el userId se pasa correctamente (el componente tiene acceso via useAuth()).
Criterios de validación
npm run buildsin errores TypeScriptnpm run test— 561 tests siguen verdes- Importar un BPMN → el proceso se carga correctamente en el workspace
- Editar el costo de una actividad → persiste después de recargar la página
- Editar la probabilidad de un gateway → persiste después de recargar
- Crear un recurso → aparece en Supabase tabla
resourcessinprocess_id - El mismo recurso es visible en todos los procesos (catálogo global)
- Eliminar un proceso → sus actividades y gateways desaparecen de Supabase (CASCADE)
- El recurso creado NO desaparece al eliminar el proceso (es global)
- En Supabase tabla
activities: aparecen las actividades conprocess_idcorrecto - En Supabase tabla
activity_resource_assignments: aparecen las asignaciones conscenario - Validación visual del director: workspace funcional (canvas + panel de actividad)
Reporte esperado al completar
- Lista de archivos creados/modificados
- Resultado de
npm run test(número de tests, pass/fail) - Resultado de
npm run build - Screenshot del workspace con actividades cargadas
- Screenshot de Supabase tabla
resources(sin columnaprocess_id) - Screenshot de Supabase tabla
activity_resource_assignmentscon al menos una fila - Cualquier decisión de iniciativa fuera del scope (clasificar nivel 1/2/3)
Notas arquitectónicas
Los tests de Dexie siguen verdes
tests/integration/indexeddb-persistence.test.ts usa el resourceRepo de Dexie con processId. Esos tests deben seguir pasando sin cambios — el resourceRepo Dexie todavía existe en repositories.ts, simplemente ya no es usado por la producción. No borrar ni modificar ese archivo en esta etapa.
Patrón userId establecido
El patrón de Etapa 2 aplica acá: repos reciben userId como parámetro, stores lo reciben de los componentes via useAuth(). Los tests de componentes que llaman acciones de escritura deben mockear useAuth.
Gateways no tienen created_by/updated_by en el schema — no necesitan userId en el repo.
Catálogo global — impacto en UX
Al cargar el workspace, ahora se cargan TODOS los recursos del equipo (no solo los del proceso). El ResourcesPanel los mostrará todos. Esto es el comportamiento correcto del catálogo global — el rediseño visual de ese panel es Etapa 7.