From 53f8a8d8cd58585a7dcac505eb9fc880789263af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcos=20Ben=C3=ADtez?= Date: Tue, 7 Jul 2026 21:12:00 -0300 Subject: [PATCH] etapa 4 terminada --- sprints/sprint-8/ETAPA_4_PROMPT.md | 622 +++++++++++++++++++++++++++++ 1 file changed, 622 insertions(+) create mode 100644 sprints/sprint-8/ETAPA_4_PROMPT.md diff --git a/sprints/sprint-8/ETAPA_4_PROMPT.md b/sprints/sprint-8/ETAPA_4_PROMPT.md new file mode 100644 index 0000000..29e381b --- /dev/null +++ b/sprints/sprint-8/ETAPA_4_PROMPT.md @@ -0,0 +1,622 @@ +# Sprint 8 — Etapa 4: Selector de área en workspace + CRUD de áreas en admin + +> **Tipo:** UI — workspace sidebar + panel de administración +> **Rama:** main +> **Estimación:** 2-3 horas +> **Dependencias:** Etapas 1, 2 y 3 completadas ✅ +> (tabla `areas` en DB, `area-repo.ts` con CRUD, `areaId`/`areaName` en tipos y repos) + +--- + +## Objetivo + +Conectar el campo `area_id` al flujo de trabajo real del usuario en dos lugares: + +1. **Workspace sidebar (`GlobalSettingsPanel.tsx`):** reemplazar el campo de texto libre "Cliente" por un `` de shadcn maneja strings. + +### Imports a agregar + +```typescript +import { supabaseAreaRepo } from '@/persistence/supabase/area-repo' +import type { Area } from '@/domain/types' +``` + +### Actualizar el `useEffect` principal + +En el `useEffect` que depende de `[currentProcess]`, reemplazar la línea `setLocalClient(currentProcess.clientName)` por: + +```typescript +setLocalAreaId(currentProcess.areaId ?? '') + +// Cargar áreas de la org del proceso +if (currentProcess.orgId) { + setAreasLoading(true) + supabaseAreaRepo.getByOrg(currentProcess.orgId) + .then(setAreas) + .catch(() => setAreas([])) + .finally(() => setAreasLoading(false)) +} else { + setAreas([]) +} +``` + +### Actualizar `handleSave` + +**Eliminar:** +```typescript +clientName: localClient.trim(), +``` + +**Agregar:** +```typescript +areaId: localAreaId === '' ? null : localAreaId, +``` + +El campo `clientName` ya no se pasa en `handleSave` — el store conservará el valor existente en el proceso (el campo persiste en DB vía el objeto actual del proceso, sin cambios desde esta pantalla). + +### Reemplazar el bloque JSX del campo "Cliente" + +**Antes (campo texto libre "Cliente"):** +```tsx +
+ + {isClientRole ? ( +

{localClient || '—'}

+ ) : ( + { setLocalClient(e.target.value); markDirty() }} + placeholder="ej. Empresa ABC" + className="h-8 text-sm" + /> + )} +
+``` + +**Después (selector de área):** +```tsx +
+ + {areasLoading ? ( +
+ ) : !currentProcess.orgId ? ( +

Sin organización asignada

+ ) : isClientRole && user?.platformRole === 'client_viewer' ? ( +

{currentProcess.areaName || '—'}

+ ) : ( + + )} +
+``` + +**Notas:** +- `client_viewer` nunca llega al workspace (redirigido en WorkspacePage). La condición `isClientRole && user?.platformRole === 'client_viewer'` es por seguridad. +- `client_editor` sí puede seleccionar área — la RLS de `areas_select` le retorna las áreas de su org. +- Si `currentProcess.orgId` es null (edge case post-migración), mostrar texto descriptivo en lugar del dropdown. +- `Select` ya está importado en el archivo. + +### Verificar que `Select` y sus sub-componentes estén importados + +Ya están importados en `GlobalSettingsPanel.tsx` (`Select`, `SelectContent`, `SelectItem`, `SelectTrigger`, `SelectValue`). Confirmar antes de agregar duplicados. + +--- + +## Parte B: `WorkspacePage.tsx` — display de areaName en topbar + +### Qué cambiar + +Reemplazar el bloque "Cliente — editable inline" (~líneas 254-276) en el topbar. + +**Antes (edición inline de clientName):** +```tsx +{/* Cliente — editable inline solo para roles internos */} +{user?.platformRole === 'client_editor' ? ( + currentProcess.clientName && ( +

{currentProcess.clientName}

+ ) +) : editingField === 'client' ? ( + setEditValue(e.target.value)} + onBlur={() => saveField('client')} + onKeyDown={(e) => { + if (e.key === 'Enter') { e.preventDefault(); saveField('client') } + if (e.key === 'Escape') setEditingField(null) + }} + placeholder="Agregar cliente…" + /> +) : ( +
{ setEditValue(currentProcess.clientName ?? ''); setEditingField('client') }} + > + {currentProcess.clientName + ?

{currentProcess.clientName}

+ :

Agregar cliente…

+ } + {savedField === 'client' + ? + : + } +
+)} +``` + +**Después (display de areaName, solo lectura):** +```tsx +{/* Área — display de solo lectura (editable desde el panel de configuración) */} +{currentProcess.areaName && ( +

{currentProcess.areaName}

+)} +``` + +### Lo que NO cambia en WorkspacePage.tsx + +- La función `saveField` — puede quedar con el caso `'client'` como código muerto, no eliminar. +- El tipo de `editingField` — no es necesario actualizar si es genérico. +- El bloque de edición inline del **nombre** del proceso — sin cambios. +- Todo el resto del archivo. + +**Restricción explícita:** NO eliminar la función `saveField` ni el estado `editingField`. Solo reemplazar el bloque JSX del cliente. El `saveField('client')` queda como código muerto — menos riesgo que eliminar y romper algo. + +--- + +## Parte C: `OrganizationDetailPage.tsx` — CRUD de áreas + +### Estructura de estado a agregar + +```typescript +// Áreas de la org +const [areas, setAreas] = useState([]) +const [areasLoading, setAreasLoading] = useState(false) + +// Sheet crear/editar área +const [areaSheetOpen, setAreaSheetOpen] = useState(false) +const [editingAreaId, setEditingAreaId] = useState(null) // null = crear nuevo +const [areaNameInput, setAreaNameInput] = useState('') +const [savingArea, setSavingArea] = useState(false) +const [areaFormError, setAreaFormError] = useState(null) + +// Confirmación de delete +const [deleteConfirmId, setDeleteConfirmId] = useState(null) +const [deletingAreaId, setDeletingAreaId] = useState(null) +``` + +### Interfaces locales a agregar (top del archivo) + +```typescript +interface AreaWithCount { + id: string + name: string + description: string | null + processCount: number +} +``` + +Y extender `OrgProcess` con `area_id`: +```typescript +interface OrgProcess { + id: string + name: string + client: string | null + area_id: string | null // 🆕 para calcular processCount por área + updated_at: string +} +``` + +### Import a agregar + +```typescript +import { supabaseAreaRepo } from '@/persistence/supabase/area-repo' +import { FolderOpen, Plus, Pencil, Trash2 } from 'lucide-react' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' +``` + +Verificar qué ya está importado antes de agregar duplicados. + +### Actualizar `fetchDetail` + +Extender el `Promise.all` para incluir la carga de áreas: + +```typescript +const fetchDetail = async () => { + const [orgRes, procRes, userRes] = await Promise.all([ + supabase.from('organizations').select('name').eq('id', orgId).single(), + supabase + .from('processes') + .select('id, name, client, area_id, updated_at') // ← agregar area_id + .eq('org_id', orgId) + .order('updated_at', { ascending: false }), + supabase.from('users').select('id, name, company, platform_role, avatar_url').eq('org_id', orgId).order('name'), + ]) + if (orgRes.data) setOrgName((orgRes.data as { name: string }).name) + const loadedProcesses = (procRes.data as OrgProcess[] | null) ?? [] + setProcesses(loadedProcesses) + setUsers((userRes.data as OrgUser[] | null) ?? []) + setLoading(false) + + // Cargar áreas y calcular conteo de procesos + setAreasLoading(true) + try { + const rawAreas = await supabaseAreaRepo.getByOrg(orgId) + const withCount: AreaWithCount[] = rawAreas.map((a) => ({ + id: a.id, + name: a.name, + description: a.description, + processCount: loadedProcesses.filter((p) => p.area_id === a.id).length, + })) + setAreas(withCount) + } catch { + setAreas([]) + } finally { + setAreasLoading(false) + } +} +``` + +**Nota:** las áreas se cargan después de los procesos para poder calcular el conteo con un solo fetch. + +### Funciones CRUD de áreas + +```typescript +const openCreateArea = () => { + setEditingAreaId(null) + setAreaNameInput('') + setAreaFormError(null) + setAreaSheetOpen(true) +} + +const openEditArea = (area: AreaWithCount) => { + setEditingAreaId(area.id) + setAreaNameInput(area.name) + setAreaFormError(null) + setAreaSheetOpen(true) +} + +const handleSaveArea = async () => { + const name = areaNameInput.trim() + if (!name) { setAreaFormError('El nombre del área es requerido'); return } + setSavingArea(true) + setAreaFormError(null) + try { + if (editingAreaId) { + await supabaseAreaRepo.update(editingAreaId, { name }) + } else { + await supabaseAreaRepo.create({ orgId, name }) + } + setAreaSheetOpen(false) + void fetchDetail() + } catch (e) { + setAreaFormError(e instanceof Error ? e.message : 'Error al guardar el área') + } finally { + setSavingArea(false) + } +} + +const handleDeleteArea = async (areaId: string) => { + setDeletingAreaId(areaId) + try { + await supabaseAreaRepo.delete(areaId) + setDeleteConfirmId(null) + void fetchDetail() + } catch (e) { + console.error('Error eliminando área:', e) + } finally { + setDeletingAreaId(null) + } +} +``` + +### Agregar tercer tab "Áreas" + +En ``, agregar después del tab de Usuarios: +```tsx + + + Áreas ({areas.length}) + +``` + +Y el contenido del tab: + +```tsx +{/* ─── Tab: Áreas ──────────────────────────────────────────────── */} + +
+

+ {areasLoading + ? 'Cargando áreas…' + : areas.length === 0 + ? 'No hay áreas en esta organización.' + : `${areas.length} área${areas.length !== 1 ? 's' : ''}.`} +

+ +
+ + {areasLoading ? ( +
+ + +
+ ) : areas.length === 0 ? ( + } + message="Las áreas agrupan procesos dentro de una organización (ej: Operaciones, RR.HH., Finanzas)." + /> + ) : ( +
+ + + + + + + + + {areas.map((area) => ( + + + + + + ))} + +
ÁreaProcesos +
{area.name} + {area.processCount} {area.processCount === 1 ? 'proceso' : 'procesos'} + + {deleteConfirmId === area.id ? ( + /* Confirmación inline */ +
+ + {area.processCount > 0 + ? `${area.processCount} proceso${area.processCount !== 1 ? 's' : ''} quedarán sin área.` + : '¿Eliminar esta área?'} + + + +
+ ) : ( +
+ + +
+ )} +
+
+ )} +
+``` + +### Agregar Sheet para crear/editar área + +Después del Sheet existente de "Asignar proceso", agregar: + +```tsx +{/* ─── Sheet: Crear/editar área ────────────────────────────────── */} + + + + + {editingAreaId ? 'Renombrar área' : `Nueva área en ${orgName}`} + + +
+
+ + { setAreaNameInput(e.target.value); setAreaFormError(null) }} + placeholder="ej. Operaciones, RR.HH., Finanzas" + className="h-9" + onKeyDown={(e) => { if (e.key === 'Enter') void handleSaveArea() }} + autoFocus + /> + {areaFormError && ( +

{areaFormError}

+ )} +
+ + + +
+
+
+``` + +--- + +## Lo que NO cambia en esta etapa + +- **`src/domain/types.ts`** — no tocar (ya tiene `areaId`, `areaName`) +- **`src/persistence/supabase/area-repo.ts`** — no tocar (CRUD ya implementado) +- **`src/persistence/supabase/process-repo.ts`** — no tocar +- **`src/store/process-store.ts`** — no tocar +- **`src/features/library/LibraryPage.tsx`** — no tocar +- **`src/features/settings/SettingsPage.tsx`** — no tocar +- **Cualquier otro archivo** — sin cambios + +**Campos `clientName` y `groupId` en el dominio:** todavía coexisten. En GlobalSettingsPanel, el campo "Cliente" se reemplaza por "Área". El `clientName` sigue en el proceso como dato histórico, solo deja de ser editable desde esta pantalla. Se eliminará del dominio en la migración de limpieza (futura Etapa de cleanup). + +--- + +## Criterios de validación + +### Funcionales (validación visual mandatoria del director antes del OK formal) + +**GlobalSettingsPanel (workspace sidebar):** +- [ ] El campo "Área" muestra un Select con las áreas de la org del proceso +- [ ] La opción "Sin área" aparece primero en el dropdown +- [ ] Seleccionar un área y guardar persiste `area_id` en el proceso (verificar en Supabase o en el Network tab) +- [ ] Al recargar la página, el área seleccionada se muestra correctamente +- [ ] Si el proceso no tiene `orgId` (edge case), el field muestra "Sin organización asignada" en lugar del dropdown +- [ ] Si la org no tiene áreas, el dropdown muestra "No hay áreas en esta organización" +- [ ] El campo "Cliente" ya no aparece en el panel + +**WorkspacePage (topbar):** +- [ ] Si el proceso tiene `areaName`, se muestra como texto de solo lectura bajo el nombre del proceso +- [ ] Si no tiene `areaName`, la segunda línea no aparece (no muestra texto vacío ni placeholder) +- [ ] Ya no hay edición inline de cliente en el topbar + +**OrganizationDetailPage (admin):** +- [ ] Existe un tercer tab "Áreas" en la página de detalle de organización +- [ ] El tab lista las áreas con nombre y conteo de procesos +- [ ] "Nueva área" abre un sheet con un input de nombre y botón Crear +- [ ] Crear un área nueva aparece en la lista (sin recargar la página manualmente) +- [ ] El botón de lápiz abre el sheet con el nombre actual pre-cargado para renombrar +- [ ] El botón de papelera muestra confirmación inline con el conteo de procesos afectados +- [ ] Confirmar delete elimina el área y la lista se actualiza +- [ ] Cancelar delete cierra la confirmación sin eliminar + +### Técnicos +- [ ] `npm run build` sin errores TypeScript +- [ ] `npm run test` sin regresiones (596 tests — esta etapa no agrega tests, pero no puede romper existentes) +- [ ] No hay `any` introducido sin justificación + +--- + +## Restricciones explícitas + +- **NO eliminar** `clientName` ni `groupId` del dominio ni de los repositorios — sigue coexistiendo +- **NO modificar** `process-store.ts` — el store ya maneja `areaId` vía partial updates +- **NO modificar** `src/domain/types.ts` — ya tiene los campos necesarios desde Etapa 2 +- **NO modificar** `area-repo.ts` — no cambiar el repo existente +- **NO agregar** nuevas dependencias — todo con el stack existente +- **NO crear** archivos de test en esta etapa — el esfuerzo de test va en Etapa 6 + +--- + +## Archivos autorizados a modificar + +``` +src/ +├── features/ +│ ├── workspace/ +│ │ ├── GlobalSettingsPanel.tsx ← Parte A: reemplazar campo Cliente → selector Área +│ │ └── WorkspacePage.tsx ← Parte B: reemplazar inline edit clientName → display areaName +│ └── admin/ +│ └── OrganizationDetailPage.tsx ← Parte C: agregar tab Áreas + CRUD +``` + +Ningún otro archivo debe modificarse. + +--- + +## Commits esperados + +``` +feat(workspace): selector de área en GlobalSettingsPanel reemplaza campo libre "Cliente" +feat(workspace): topbar muestra areaName en lugar de clientName inline-editable +feat(admin): CRUD de áreas en OrganizationDetailPage — tab Áreas con crear/renombrar/eliminar +``` + +--- + +## Reporte esperado + +1. Confirmar que el campo "Cliente" en `GlobalSettingsPanel` ya no existe y fue reemplazado por el selector de área +2. Confirmar que el topbar de WorkspacePage ya no tiene edición inline de clientName +3. Confirmar que `OrganizationDetailPage` tiene un tercer tab "Áreas" funcional +4. Lista compacta de archivos modificados con resumen de cambios +5. Resultado de `npm run build` y `npm run test` (con conteo de tests) +6. **Solicitar validación visual del director** — describir el estado de la UI en los tres lugares para que el director valide en la app antes del OK formal