Corrección 1 — Delete de proceso restaurado + Corrección 2 — Patrón userId en repos
This commit is contained in:
392
sprints/sprint-4/ETAPA_3_PROMPT.md
Normal file
392
sprints/sprint-4/ETAPA_3_PROMPT.md
Normal file
@@ -0,0 +1,392 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user