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

393 lines
15 KiB
Markdown

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