This commit is contained in:
343
sprints/sprint-8/ETAPA_5_PROMPT.md
Normal file
343
sprints/sprint-8/ETAPA_5_PROMPT.md
Normal file
@@ -0,0 +1,343 @@
|
||||
# Sprint 8 — Etapa 5: client_editor puede importar BPMN
|
||||
|
||||
> **Tipo:** DB + UI — habilitar flujo de importación para rol cliente editor
|
||||
> **Rama:** main
|
||||
> **Estimación:** 1.5 horas
|
||||
> **Dependencias:** Etapas 1–4 completadas ✅
|
||||
> (tabla `areas` y columna `org_id` en processes ya existen, `isClientRole` ya implementado en workspace)
|
||||
|
||||
---
|
||||
|
||||
## Objetivo
|
||||
|
||||
Habilitar que un usuario con rol `client_editor` pueda importar archivos BPMN propios. Actualmente la `ImportPage` redirige a todos los roles cliente a `/`. Con esta etapa, `client_editor` pasa a ser un ciudadano de primera clase del flujo de importación: puede subir un BPMN, el proceso se asigna automáticamente a su org, y puede trabajar desde el workspace como lo hace InQuality.
|
||||
|
||||
`client_viewer` **no** obtiene acceso de importación — eso no cambia.
|
||||
|
||||
---
|
||||
|
||||
## Contexto del código actual
|
||||
|
||||
Leer estos archivos antes de modificar:
|
||||
|
||||
- `src/features/import/ImportPage.tsx` — tiene dos guards que bloquean a client_editor:
|
||||
1. `useEffect` con `allowed = ['platform_admin', 'member']` → redirige a `/`
|
||||
2. `if (isClientRole) return null` antes del return final → nunca llega al JSX
|
||||
Además, el proceso se crea con `orgId: null`, lo que rompe las RLS.
|
||||
- `src/features/library/LibraryPage.tsx` — el botón "Importar BPMN" del header está envuelto en `{!isClientRole && ...}` (líneas ~841-860). El empty state de la vista cliente muestra "Contactá a tu administrador" sin botón de importar para nadie.
|
||||
- `supabase/migrations/015_client_roles_and_rls.sql` — la política `insert_processes` permite solo `platform_admin` y `member`. La línea de comentario lo dice explícitamente: `-- client_editor y client_viewer NO pueden crear procesos nuevos`.
|
||||
|
||||
---
|
||||
|
||||
## Parte 1: Migration 018
|
||||
|
||||
Crear `supabase/migrations/018_client_editor_insert.sql`.
|
||||
|
||||
El único cambio de esta migración: extender la política `insert_processes` para incluir a `client_editor` cuando inserta en su propia org.
|
||||
|
||||
```sql
|
||||
-- Permite que client_editor inserte procesos en su propia org
|
||||
-- Los demás roles mantienen sus permisos actuales
|
||||
|
||||
DROP POLICY IF EXISTS "insert_processes" ON public.processes;
|
||||
|
||||
CREATE POLICY "insert_processes" ON public.processes
|
||||
FOR INSERT WITH CHECK (
|
||||
public.is_platform_admin()
|
||||
OR public.current_platform_role() = 'member'
|
||||
OR (
|
||||
public.current_platform_role() = 'client_editor'
|
||||
AND org_id = public.current_org()
|
||||
)
|
||||
-- client_viewer y guest siguen sin poder crear procesos
|
||||
);
|
||||
```
|
||||
|
||||
**Verificar que las políticas de actividades y gateways ya permiten a client_editor:**
|
||||
Buscar en `015_client_roles_and_rls.sql`:
|
||||
- `activities_insert`: usa `NOT IN ('client_viewer', 'guest')` → client_editor puede ✅
|
||||
- `gateways_write`: usa `NOT IN ('client_viewer', 'guest')` → client_editor puede ✅
|
||||
|
||||
Si alguna de esas dos NO incluye a client_editor, corregir en esta misma migración. Verificar antes de cerrar el archivo.
|
||||
|
||||
### Aplicar la migración
|
||||
|
||||
Aplicar en Supabase y verificar:
|
||||
|
||||
```sql
|
||||
-- Verificar que la nueva política existe
|
||||
SELECT policyname, cmd, qual
|
||||
FROM pg_policies
|
||||
WHERE schemaname = 'public' AND tablename = 'processes';
|
||||
-- Esperado: ver "insert_processes" con la nueva definición que incluye client_editor
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Parte 2: `ImportPage.tsx`
|
||||
|
||||
### 2A — Eliminar los dos guards que bloquean a client_editor
|
||||
|
||||
**Guard 1 — cambiar la lista de roles permitidos en el `useEffect`:**
|
||||
|
||||
```typescript
|
||||
// Antes:
|
||||
const allowed = ['platform_admin', 'member']
|
||||
if (!allowed.includes(user.platformRole)) {
|
||||
void navigate({ to: '/' })
|
||||
}
|
||||
|
||||
// Después:
|
||||
const allowed = ['platform_admin', 'member', 'client_editor']
|
||||
if (!allowed.includes(user.platformRole)) {
|
||||
void navigate({ to: '/' })
|
||||
}
|
||||
```
|
||||
|
||||
**Guard 2 — reemplazar el bloqueo total por uno solo para client_viewer:**
|
||||
|
||||
```typescript
|
||||
// Antes (bloquea a todos los client roles):
|
||||
const isClientRole = user?.platformRole === 'client_editor' || user?.platformRole === 'client_viewer'
|
||||
if (isClientRole) return null
|
||||
|
||||
// Después (solo bloquea client_viewer):
|
||||
if (user?.platformRole === 'client_viewer') return null
|
||||
```
|
||||
|
||||
### 2B — Asignar `orgId` del usuario en el proceso creado
|
||||
|
||||
Al construir el objeto `process` dentro de `processFile`, el campo `orgId` actualmente es `null`. Para que la RLS de `insert_processes` valide `org_id = current_org()`, el proceso insertado debe tener el `org_id` del usuario.
|
||||
|
||||
```typescript
|
||||
// Antes:
|
||||
const process: Process = {
|
||||
// ...
|
||||
orgId: null,
|
||||
// ...
|
||||
}
|
||||
|
||||
// Después:
|
||||
const process: Process = {
|
||||
// ...
|
||||
orgId: user!.orgId, // ← org_id del usuario autenticado; null solo si no tiene org (no debería pasar para client_editor)
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
Verificar que `user.orgId` está disponible en el tipo `User` del AuthContext (debería estar — se pobló en Sprint 7).
|
||||
|
||||
### 2C — Ocultar los procesos de ejemplo para client_editor
|
||||
|
||||
Los "procesos de ejemplo" (`SAMPLE_PROCESSES`) son demostraciones internas de InQuality. Un client_editor no debería importar datos de prueba de InQuality en su propio espacio.
|
||||
|
||||
En el JSX, la sección de ejemplos está bajo el drop zone. Envolverla en una condición:
|
||||
|
||||
```tsx
|
||||
// Antes: siempre visible
|
||||
<div className="mt-8 w-full max-w-lg">
|
||||
<p ...>O cargá un proceso de ejemplo con un click</p>
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
{SAMPLE_PROCESSES.map(...)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
// Después: solo visible para InQuality (platform_admin y member)
|
||||
{user?.platformRole !== 'client_editor' && (
|
||||
<div className="mt-8 w-full max-w-lg">
|
||||
<p ...>O cargá un proceso de ejemplo con un click</p>
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
{SAMPLE_PROCESSES.map(...)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
```
|
||||
|
||||
### 2D — Corregir texto de marca en ImportPage
|
||||
|
||||
El header tiene texto desactualizado que viola las reglas de marca del producto:
|
||||
|
||||
```tsx
|
||||
// Antes (incorrecto — nombre viejo del producto):
|
||||
<div className="inline-flex items-center gap-2 ...">
|
||||
<FlaskConical className="h-3.5 w-3.5" />
|
||||
Process Cost Platform · MVP Fase 0
|
||||
</div>
|
||||
|
||||
// Después:
|
||||
<div className="inline-flex items-center gap-2 ...">
|
||||
<FlaskConical className="h-3.5 w-3.5" />
|
||||
InQ ROI
|
||||
</div>
|
||||
```
|
||||
|
||||
El tagline del final de la página también está desactualizado:
|
||||
|
||||
```tsx
|
||||
// Antes:
|
||||
<p className="mt-8 text-xs text-slate-300">
|
||||
100% en tu navegador · Sin backend · Sin registro · Datos guardados localmente
|
||||
</p>
|
||||
|
||||
// Después: eliminar este párrafo (el producto ya tiene backend y registro)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Parte 3: `LibraryPage.tsx`
|
||||
|
||||
### 3A — Botón "Importar BPMN" en el header visible para client_editor
|
||||
|
||||
El botón en el header de LibraryPage (líneas ~841-860) está condicionado con `!isClientRole`, lo que lo oculta para todos los roles cliente. Cambiar para mostrarlo a `client_editor`:
|
||||
|
||||
```tsx
|
||||
// Antes:
|
||||
{!isClientRole && (
|
||||
<button
|
||||
onClick={handleImport}
|
||||
className="..."
|
||||
style={{ background: '#F59845' }}
|
||||
>
|
||||
<Upload className="h-4 w-4" />
|
||||
Importar BPMN
|
||||
</button>
|
||||
)}
|
||||
|
||||
// Después:
|
||||
{user?.platformRole !== 'client_viewer' && (
|
||||
<button
|
||||
onClick={handleImport}
|
||||
className="..."
|
||||
style={{ background: '#F59845' }}
|
||||
>
|
||||
<Upload className="h-4 w-4" />
|
||||
Importar BPMN
|
||||
</button>
|
||||
)}
|
||||
```
|
||||
|
||||
### 3B — Empty state diferenciado para client_editor vs client_viewer
|
||||
|
||||
En `HomeView`, el empty state para roles cliente (cuando `groups.length === 0 && processes.length === 0`) actualmente muestra "Contactá a tu administrador" para todos. Diferenciarlo:
|
||||
|
||||
```tsx
|
||||
// Antes (igual para todos los client roles):
|
||||
if (isClientRole) {
|
||||
return (
|
||||
<div ...>
|
||||
<Building2 ... />
|
||||
<p>No tenés procesos asignados aún</p>
|
||||
<p>Contactá a tu administrador para que te asigne acceso</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Después (diferenciado):
|
||||
if (isClientRole) {
|
||||
if (user?.platformRole === 'client_editor') {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center py-24 gap-4">
|
||||
<Building2 className="h-16 w-16 text-muted-foreground/30" />
|
||||
<p className="text-base font-medium">Tu biblioteca está vacía</p>
|
||||
<p className="text-[13px] text-muted-foreground">
|
||||
Importá tu primer BPMN para empezar a cuantificar costos
|
||||
</p>
|
||||
<button
|
||||
onClick={() => onImport()}
|
||||
className="flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium text-white transition-opacity hover:opacity-90"
|
||||
style={{ background: '#F59845' }}
|
||||
>
|
||||
<Upload className="h-4 w-4" />
|
||||
Importar BPMN
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
// client_viewer: mantener mensaje existente
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center py-24 gap-4">
|
||||
<Building2 className="h-16 w-16 text-muted-foreground/30" />
|
||||
<p className="text-base font-medium">No tenés procesos asignados aún</p>
|
||||
<p className="text-[13px] text-muted-foreground">Contactá a tu administrador para que te asigne acceso</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
`user` está disponible en el scope de `HomeView` como prop (o via `useAuth()` — verificar cuál aplica).
|
||||
|
||||
---
|
||||
|
||||
## Lo que NO cambia en esta etapa
|
||||
|
||||
- **`process-repo.ts`** — no tocar. `updateEditable()` no necesita `bpmn_xml` para este flujo.
|
||||
- **`process-store.ts`** — no tocar.
|
||||
- **`WorkspacePage.tsx`** — sin cambios. client_editor ya tiene acceso al workspace desde Sprint 7 (la RLS de `update_processes` ya lo cubre).
|
||||
- **`GlobalSettingsPanel.tsx`** — sin cambios. El selector de área ya funciona para client_editor desde Etapa 4.
|
||||
- **Rol `client_viewer`** — sigue sin acceso a importación en todo momento.
|
||||
|
||||
---
|
||||
|
||||
## Criterios de validación
|
||||
|
||||
### Técnicos
|
||||
- [ ] Migración 018 creada y aplicada en Supabase
|
||||
- [ ] `insert_processes` en la DB incluye la cláusula de client_editor (verificar con `pg_policies`)
|
||||
- [ ] `npm run build` sin errores TypeScript
|
||||
- [ ] `npm run test` sin regresiones (596 tests)
|
||||
|
||||
### Funcionales (validación visual mandatoria del director)
|
||||
- [ ] Loguear como `client_editor` → `/library` → botón "Importar BPMN" visible en el header
|
||||
- [ ] Hacer click en "Importar BPMN" → lleva a `/import` (no redirige a `/`)
|
||||
- [ ] `/import` muestra el drop zone; la sección de "procesos de ejemplo" NO aparece
|
||||
- [ ] Subir un archivo BPMN real → proceso creado → workspace abre correctamente
|
||||
- [ ] En Supabase: verificar que el proceso creado tiene `org_id` = org del client_editor (no null)
|
||||
- [ ] Loguear como `client_viewer` → `/library` → botón "Importar BPMN" NO visible
|
||||
- [ ] Si client_viewer navega manualmente a `/import` → redirige a `/`
|
||||
- [ ] La biblioteca vacía de client_editor muestra botón de importar; la de client_viewer muestra "Contactá a tu administrador"
|
||||
|
||||
---
|
||||
|
||||
## Restricciones explícitas
|
||||
|
||||
- **NO** tocar políticas RLS de tablas distintas a `processes` (activities y gateways ya permiten client_editor)
|
||||
- **NO** crear procesos de muestra en la DB para client_editor — el flujo es importar BPMNs reales
|
||||
- **NO** modificar el workspace ni el reporte — client_editor ya puede usarlos
|
||||
- **NO** agregar tests en esta etapa
|
||||
|
||||
---
|
||||
|
||||
## Archivos autorizados a modificar/crear
|
||||
|
||||
```
|
||||
supabase/migrations/
|
||||
└── 018_client_editor_insert.sql ← crear y aplicar
|
||||
|
||||
src/features/
|
||||
├── import/
|
||||
│ └── ImportPage.tsx ← guards + orgId + ejemplos + texto marca
|
||||
└── library/
|
||||
└── LibraryPage.tsx ← botón header + empty state diferenciado
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Commits esperados
|
||||
|
||||
```
|
||||
feat(db): migración 018 — INSERT policy para client_editor en processes
|
||||
feat(import): client_editor puede importar BPMN; orgId asignado automáticamente
|
||||
feat(library): botón Importar BPMN visible para client_editor; empty state diferenciado
|
||||
fix(import): corregir texto de marca — "Process Cost Platform" → "InQ ROI"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Reporte esperado
|
||||
|
||||
1. Contenido exacto de `018_client_editor_insert.sql`
|
||||
2. Resultado de la query de verificación en `pg_policies` para `insert_processes`
|
||||
3. Lista de cambios en `ImportPage.tsx` (resumen compacto)
|
||||
4. Lista de cambios en `LibraryPage.tsx` (resumen compacto)
|
||||
5. `npm run build` y `npm run test` (con conteo)
|
||||
6. **Solicitar validación visual del director** — describir el estado de la UI para client_editor antes del OK formal
|
||||
Reference in New Issue
Block a user