Files
inq-roi-simulador-web/sprints/sprint-4/ETAPA_3_PROMPT.md

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.tsx con useAuth() funcionando
  • src/persistence/supabase/ con process-repo, group-repo, settings-repo

Alcance estricto — qué entra en esta etapa

  1. Scripts SQL para resources, activities, activity_resource_assignments, gateways
  2. Nuevos repos Supabase para las cuatro entidades
  3. Actualizar process-store.ts para usar repos Supabase
  4. Actualizar useReportData.ts para cargar recursos desde Supabase
  5. Actualizar ResourcesPanel.tsx mínimamente: crear recursos sin processId
  6. Actualizar process-repo.ts delete: eliminar limpieza Dexie de activities/gateways/resources (pasan a CASCADE en Supabase); conservar limpieza Dexie solo para simulaciones
  7. Modificar tipo Resource en src/domain/types.ts: hacer processId opcional (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 build sin errores TypeScript
  • npm 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 resources sin process_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 con process_id correcto
  • En Supabase tabla activity_resource_assignments: aparecen las asignaciones con scenario
  • Validación visual del director: workspace funcional (canvas + panel de actividad)

Reporte esperado al completar

  1. Lista de archivos creados/modificados
  2. Resultado de npm run test (número de tests, pass/fail)
  3. Resultado de npm run build
  4. Screenshot del workspace con actividades cargadas
  5. Screenshot de Supabase tabla resources (sin columna process_id)
  6. Screenshot de Supabase tabla activity_resource_assignments con al menos una fila
  7. 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.