# 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`

```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`

```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`

```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`

```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`:

```typescript
// 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.

```typescript
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.

```typescript
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`

```typescript
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:

```typescript
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:

```typescript
// 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.
