393 lines
15 KiB
Markdown
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.
|