Sprint 4 — Etapa 7 completada. Build limpio, 521/521 tests verdes. Resumen:
Some checks failed
/ Deploy to Cloudflare Pages (push) Has been cancelled

Tipos: Resource.processId? eliminado (vestigial); ResourceType ya tenía los 5 valores correctos, sin cambios.

resource-repo.ts: agregado getAllWithCreator() con JOIN a users!created_by (mismo patrón que Etapa 6) y tipo ResourceWithCreator. delete() ahora valida que no haya asignaciones activas en activity_resource_assignments antes de borrar — lanza error explicativo si las hay. Verifiqué ambos contra Supabase real: el JOIN funciona (FK reconocida por PostgREST) y el conteo de guarda también.

Nuevo — src/features/resources/:

resource-display.tsx — labels, colores/íconos por tipo (Lucide: User/Briefcase/Server/Wrench/Package) y ResourceTypeAvatar compartido
ResourceFormSheet.tsx — Sheet reutilizable para crear/editar, usado tanto en el catálogo como en el panel del workspace
ResourceCatalogPage.tsx — página /recursos: búsqueda, filtro por tipo, tabla con badge de tipo, costo, "creado por", editar/eliminar con confirmación
Nuevo — src/components/ui/sheet.tsx: componente shadcn Sheet (no existía), construido sobre @radix-ui/react-dialog que ya estaba instalado — no agregué dependencias nuevas.

ResourcesPanel.tsx rediseñado: ahora solo muestra recursos usados en el proceso actual (derivado de assignedResources), con conteo de actividades por recurso. CRUD completo se mudó a /recursos; el panel solo permite "Nuevo" (catálogo global) y tiene el link "Ver catálogo completo".

Router: ruta /recursos registrada.

ActivityPanel no se tocó, como indicaba el scope.
This commit is contained in:
2026-06-19 00:01:46 -03:00
parent 4f51c74717
commit 892b8b3bc7
14 changed files with 789 additions and 189 deletions

View File

@@ -0,0 +1,202 @@
# Sprint 4 — Etapa 7: ResourcesPanel — UX de catálogo global
## Contexto
InQ ROI — React 18 + Vite + TypeScript + TanStack Router + Zustand + shadcn/ui + Tailwind.
Etapas 16 completadas. El backend de recursos ya es global desde Etapa 3: `resource-repo.ts` hace `getAll()` sin filtro, el upsert no envía `processId`, y la tabla `resources` en Supabase no tiene columna `process_id`.
Lo que falta es UX: el `ResourcesPanel` todavía muestra todos los recursos sin filtrar (con el header "Recursos del proceso" que ya no corresponde) y no existe página de catálogo.
**Este prompt tiene un mockup aprobado como contrato visual. Claude Code lo replica sin interpretar libremente.**
---
## Mockup aprobado — descripción exacta
### Vista 1: ResourcesPanel en workspace (panel lateral derecho)
**Header del panel:**
- Título: "Recursos" (no "del proceso")
- Botón naranja `#F59845` con ícono `+` y texto "Nuevo" — abre un Sheet/Dialog para crear recurso en el catálogo global
**Cuerpo del panel:**
- Subheader: "Usados en este proceso" + cuenta `(N)`
- Lista de recursos que tienen al menos una asignación en las actividades del proceso actual
- Cada ítem: avatar circular con ícono del tipo (ver iconos y colores más abajo) + nombre del recurso + costo/hora a la derecha
- Segunda línea: tipo en texto + "N actividades" (contar cuántas actividades de este proceso usan este recurso)
- Si no hay recursos asignados: estado vacío con mensaje "Ningún recurso asignado a actividades aún"
**Footer del panel:**
- Texto de contexto: "Recursos del catálogo global del equipo"
- Botón secundario ancho completo: ícono `external-link` + "Ver catálogo completo" → navega a `/recursos`
**La lista de recursos "usados en este proceso" se deriva así:**
```typescript
const usedResourceIds = new Set(
currentProcess.activities
.flatMap(a => a.assignedResources ?? [])
.map(ar => ar.resourceId)
)
const usedResources = resources.filter(r => usedResourceIds.has(r.id))
const usageCount = (resourceId: string) =>
currentProcess.activities.filter(
a => (a.assignedResources ?? []).some(ar => ar.resourceId === resourceId)
).length
```
---
### Vista 2: Página catálogo `/recursos`
**Header de la página:**
- Título: "Catálogo de recursos"
- Subtítulo con ícono de equipo: "Compartido con todo el equipo · N recursos"
- Botón naranja `#F59845`: "+ Nuevo recurso" → abre Sheet/Dialog de creación
**Barra de herramientas:**
- Input de búsqueda (filtra por nombre en tiempo real, client-side)
- Select de tipo (filtra por tipo: "Todos los tipos" + los 5 tipos)
**Tabla:**
| Recurso | Tipo | Costo/h | Creado por | Acciones |
|---------|------|---------|-----------|---------|
| [avatar + nombre] | [badge] | [mono] | [nombre] | [edit] [delete] |
- Botones de acción: editar (abre Sheet con form pre-poblado) y eliminar (con confirmación)
- "Creado por" se resuelve desde el JOIN con `users` (igual que en LibraryPage — mismo patrón de Etapa 6)
---
## Tipos de recurso — valores definitivos
**Los únicos valores válidos son:** `person | role | system | equipment | supply`
Estos son los valores del schema real (migración 005). Actualizar `ResourceType` en `domain/types.ts`:
```typescript
export type ResourceType = 'person' | 'role' | 'system' | 'equipment' | 'supply'
```
**Labels en español para la UI:**
```typescript
const RESOURCE_TYPE_LABELS: Record<ResourceType, string> = {
person: 'Persona',
role: 'Rol',
system: 'Sistema',
equipment: 'Equipo',
supply: 'Insumo',
}
```
**Íconos y colores por tipo (Lucide React / Tabler):**
```typescript
// Usando Lucide React (ya en el stack):
person: User fondo #FEF3E7, ícono #F59845
role: Briefcase fondo #FEF3E7, ícono #D97706
system: Server fondo #EEF2FF, ícono #6366F1
equipment: Wrench fondo #F0FDF4, ícono #16A34A
supply: Package fondo #F1F5F9, ícono #64748B
```
---
## Alcance estricto — qué entra en esta etapa
### 1. Limpieza de tipos
En `src/domain/types.ts`:
- Actualizar `ResourceType` a los 5 valores definitivos (ver arriba)
- Eliminar `processId?: UUID` de la interfaz `Resource` — es un campo vestigial sin uso. Ya no se mapea ni se envía
En `src/persistence/supabase/resource-repo.ts`:
- Eliminar cualquier referencia a `processId` si existe en `fromRow` o `toRow`
### 2. Página de catálogo — nueva ruta `/recursos`
Crear `src/features/resources/ResourceCatalogPage.tsx` con:
- Header + búsqueda + filtro por tipo + tabla (ver mockup)
- Sheet/Dialog compartido para crear y editar recursos (reutilizar el mismo componente para ambas acciones)
- Confirmación antes de eliminar (no eliminar recursos que están asignados a actividades — mostrar error explicativo si hay asignaciones activas)
**JOIN con users para "Creado por"** (mismo patrón que Etapa 6):
```typescript
// En resource-repo.ts, método getAll():
const { data } = await supabase
.from('resources')
.select('*, creator:users!created_by(id, name)')
.order('name', { ascending: true })
```
Agregar tipo extendido `ResourceWithCreator` en `resource-repo.ts` (igual que `ProcessWithUserNames` en Etapa 6).
**Ruta nueva en TanStack Router:** `src/router.tsx` o donde estén definidas las rutas.
### 3. Navegación al catálogo
Agregar acceso a `/recursos` desde la app. Opciones (elegir la más limpia según la estructura de nav existente):
- Link en AppHeader junto al logo/título
- O link desde el botón "Ver catálogo completo" del ResourcesPanel (este ya está en el mockup)
- No agregar ítem de navegación lateral si no existe nav lateral — el acceso desde el panel es suficiente para Sprint 4
### 4. ResourcesPanel — redesign
Reemplazar el cuerpo actual de `src/features/workspace/ResourcesPanel.tsx` según el mockup aprobado:
- Lista filtrada "usados en este proceso" (ver derivación arriba)
- Botón "Nuevo" → Sheet/Dialog de creación (el mismo componente de la página de catálogo)
- Link "Ver catálogo completo" → navega a `/recursos`
- El CRUD completo (editar, eliminar, listar todos) vive en `/recursos`, no en el panel del workspace
### 5. ActivityPanel — sin cambios
El dropdown de asignación de recursos ya funciona correctamente con el catálogo global. No tocar.
---
## Qué NO entra en esta etapa
- Gestión de grupos de usuarios
- Permisos por recurso (los recursos son siempre visibles para todos los autenticados)
- Import/export de catálogo
- Historial de uso de un recurso (cuántos procesos lo usan)
- Cambios en PDF, simulación o workspace salvo ResourcesPanel
- Tests de integración de auth → Etapa 8
---
## Orden de ejecución
1. Actualizar `ResourceType` en `domain/types.ts` + eliminar `processId?` de `Resource`
2. Limpiar `resource-repo.ts` (eliminar processId, agregar JOIN con users)
3. Crear `src/features/resources/ResourceCatalogPage.tsx`
4. Registrar la ruta `/recursos` en el router
5. Redesign de `ResourcesPanel.tsx` (mockup como ground truth)
6. `npm run build` — debe pasar sin errores TypeScript
7. `npm run test` — debe mantener 521+ tests verdes
8. Commit: `feat(sprint-4/etapa-7): catálogo global de recursos + redesign ResourcesPanel`
---
## Criterios de validación
- [ ] `ResourceType` tiene exactamente `'person' | 'role' | 'system' | 'equipment' | 'supply'`
- [ ] `Resource` no tiene campo `processId`
- [ ] Ruta `/recursos` existe y carga la página de catálogo
- [ ] Tabla del catálogo muestra nombre, tipo (badge con color), costo/h, creado por
- [ ] Búsqueda client-side funciona por nombre
- [ ] Filtro por tipo funciona
- [ ] Crear recurso desde catálogo funciona (guarda en Supabase, aparece en la lista)
- [ ] Editar recurso funciona
- [ ] Eliminar recurso funciona (con confirmación)
- [ ] ResourcesPanel en workspace muestra SOLO recursos usados en el proceso actual
- [ ] ResourcesPanel "Ver catálogo completo" navega a `/recursos`
- [ ] ResourcesPanel "Nuevo" crea en el catálogo global
- [ ] ActivityPanel sigue funcionando (asignación de recursos sin cambios)
- [ ] Build limpio sin errores TypeScript
- [ ] 521+ tests verdes
- [ ] **Validación visual del director** — ver ResourcesPanel con lista filtrada y página `/recursos` con tabla antes del OK formal
---
## Nota: Sheet vs Modal para el form de recurso
Usar `Sheet` de shadcn/ui (panel lateral deslizante) en lugar de `Dialog` (modal centrado) — es más consistente con el contexto del workspace donde el espacio es lateral. El Sheet se abre desde ambos lugares: botón "Nuevo" del ResourcesPanel y botón "+ Nuevo recurso" de la página de catálogo. Reutilizar el mismo componente `ResourceFormSheet.tsx` en ambos contextos.