# Sprint 5 — Etapa 4: Procesos por recurso + campo empresa en perfil ## Contexto InQ ROI — React 18 + Vite + TypeScript + TanStack Router + Zustand + shadcn/ui + Tailwind + Supabase. Etapas 1, 2, 3 y 3B completadas con OK formal. Esta etapa agrega dos features pequeñas de producto. --- ## Feature A — Ver procesos asociados a un recurso ### Contexto actual `ResourceCatalogPage` tiene 5 columnas: Recurso, Tipo, Costo/h, Creado por, Acciones. El botón Eliminar ya protege contra borrado de recursos en uso (verifica `activity_resource_assignments`), pero el usuario no puede ver qué procesos usan el recurso antes de intentar eliminarlo. ### Implementación **Paso 1 — Nueva query en `resource-repo.ts`:** Agregar un método `getProcessCountsPerResource()` que devuelva un map de `resourceId → cantidad de procesos distintos`. Implementar con una sola query (no una por recurso) para evitar N+1: ```ts async getProcessCountsPerResource(): Promise> { const { data, error } = await supabase .from('activity_resource_assignments') .select('resource_id, activity:activities!inner(process_id)') if (error) throw error if (!data) return {} // Agregar client-side: contar procesos DISTINTOS por recurso const processSets: Record> = {} for (const row of data) { const resourceId = row.resource_id const processId = (row.activity as { process_id: string }).process_id if (!processSets[resourceId]) processSets[resourceId] = new Set() processSets[resourceId].add(processId) } return Object.fromEntries( Object.entries(processSets).map(([k, v]) => [k, v.size]) ) } ``` **Paso 2 — Cargar los conteos en `ResourceCatalogPage`:** Al cargar los recursos (`useEffect` de mount), también llamar a `getProcessCountsPerResource()` en paralelo: ```ts const [resources, processCounts] = await Promise.all([ supabaseResourceRepo.getAllWithCreator(), supabaseResourceRepo.getProcessCountsPerResource(), ]) ``` Guardar `processCounts` en estado local del componente. **Paso 3 — Nueva columna "Procesos" en la tabla:** Agregar columna entre "Creado por" y "Acciones": - Header: "Procesos" - Si `processCounts[resource.id] === 0` o undefined: mostrar "—" en color muted - Si `processCounts[resource.id] > 0`: mostrar badge clicable con el número ```tsx {count > 0 ? ( ) : ( )} ``` El color naranja InQ (`text-orange-600`, `bg-orange-50`) es correcto para indicar "hay uso activo". **Paso 4 — Expansión inline con lista de procesos:** Al hacer click en el badge, expandir una fila adicional debajo con los nombres de los procesos. Para esto necesitás una query adicional que devuelva los nombres — la query anterior solo devuelve conteos. Agregar un segundo método en `resource-repo.ts`: ```ts async getProcessesForResource(resourceId: string): Promise<{ id: string; name: string }[]> { const { data, error } = await supabase .from('activity_resource_assignments') .select('activity:activities!inner(process:processes!inner(id, name))') .eq('resource_id', resourceId) if (error) throw error if (!data) return [] // Deduplicar por proceso id const seen = new Set() const result: { id: string; name: string }[] = [] for (const row of data) { const process = (row.activity as any).process if (!seen.has(process.id)) { seen.add(process.id) result.push({ id: process.id, name: process.name }) } } return result } ``` Esta query se llama **on demand** (solo cuando el usuario expande la fila), no al cargar la página. La fila expandida muestra: - Lista de nombres de proceso, cada uno como link a `/workspace/:id` - Usar `` de TanStack Router ```tsx // Fila expandida (colspan completo de la tabla):
{processes.map(p => ( {p.name} ))}
``` --- ## Feature B — Campo empresa en perfil de usuario ### Contexto actual `AuthUser` en `/src/auth/types.ts` tiene: `id, email, name, avatarUrl?, platformRole`. Sin `company`. `buildAuthUser()` construye el objeto desde OAuth `user_metadata` de Google. No lee la tabla `users` de Supabase directamente. El AppHeader dropdown muestra: avatar + nombre + email + botón "Cerrar sesión". ### Implementación **Paso 1 — Migración en Supabase:** Crear archivo de migración `supabase/migrations/006_add_company_to_users.sql`: ```sql ALTER TABLE users ADD COLUMN IF NOT EXISTS company text; ``` Ejecutar en Supabase Studio SQL Editor. Verificar que no rompe datos existentes (la columna es nullable sin default). **Paso 2 — Extender `AuthUser`:** En `/src/auth/types.ts`: ```ts export interface AuthUser { id: string email: string name: string avatarUrl?: string platformRole: 'platform_admin' | 'member' | 'guest' company?: string // 🆕 nullable, viene de tabla users } ``` **Paso 3 — Leer `company` desde la tabla `users`:** `buildAuthUser()` actualmente solo usa `user_metadata` de OAuth. Para `company` necesitamos leer la tabla `users` de Supabase (que ya existe y se populó en Sprint 4). La forma más limpia: después de que el usuario autentique, hacer una query puntual a `users` para obtener el `company`. Este fetch puede ir en el `onAuthStateChange` callback, justo después de construir el `AuthUser` base: ```ts // En AuthContext o SupabaseAuthService, después de obtener el usuario de OAuth: const { data: userRow } = await supabase .from('users') .select('company, name') .eq('id', user.id) .maybeSingle() const authUser: AuthUser = { ...buildAuthUser(session.user), company: userRow?.company ?? undefined, // Si el nombre fue editado en la app (no solo el de Google), usar el de la tabla: name: userRow?.name ?? buildAuthUser(session.user).name, } ``` **Paso 4 — UI en AppHeader: mostrar empresa** En el dropdown del AppHeader, agregar la empresa debajo del email si está definida: ```tsx
{user.name} {user.email} {user.company && ( {user.company} )}
``` **Paso 5 — Edición de perfil:** Agregar un item "Editar perfil" en el dropdown (antes de "Cerrar sesión"), que abre un `Sheet` (el mismo componente Sheet de shadcn/ui ya usado en `ResourceFormSheet`): El Sheet de perfil tiene: - **Nombre** — input editable (texto libre) - **Empresa** — input editable (texto libre, placeholder "InQuality") - **Email** — solo lectura (viene de Google OAuth, no editable) - Botón "Guardar" — hace `UPDATE users SET name = ?, company = ? WHERE id = ?` - Botón "Cancelar" Al guardar: actualizar el store de auth local para que el AppHeader refleje el cambio sin recargar. ```ts // En el repo o en el store, al guardar perfil: await supabase .from('users') .update({ name, company }) .eq('id', user.id) // Actualizar el AuthUser en el store local: updateUser({ name, company }) ``` `updateUser` debe existir en `AuthContext` o `useAuth` — si no existe, crearlo. --- ## Qué NO entra en esta etapa - Cambios en la lógica de simulación o heatmap - Gestión de roles o permisos de usuario - Página de perfil separada (el Sheet del dropdown es suficiente) - Campos adicionales más allá de nombre y empresa --- ## Orden de ejecución 1. Migración SQL `006_add_company_to_users.sql` — ejecutar en Supabase + crear archivo 2. Extender `AuthUser` con `company?` en `src/auth/types.ts` 3. Modificar `buildAuthUser` / `onAuthStateChange` para leer `company` y `name` de tabla `users` 4. AppHeader: mostrar empresa en dropdown si existe 5. AppHeader: agregar "Editar perfil" → Sheet con nombre/empresa editables 6. `resource-repo.ts`: agregar `getProcessCountsPerResource()` y `getProcessesForResource()` 7. `ResourceCatalogPage`: cargar conteos en paralelo con recursos, agregar columna "Procesos" 8. `ResourceCatalogPage`: expansión inline de fila con lista de procesos linkeados 9. `npm run build` — 0 errores TypeScript 10. `npm run lint` — 0 warnings nuevos 11. `npm run test` — mantener 528+ tests Vitest verdes 12. Commit: `feat(sprint-5/etapa-4): procesos por recurso + campo empresa en perfil` --- ## Criterios de validación - [ ] Columna "Procesos" visible en `/recursos` con conteo correcto (verificar con un recurso usado en ≥1 proceso) - [ ] Recurso sin asignaciones muestra "—" en la columna Procesos - [ ] Click en el badge expande la lista de procesos con nombres y links funcionales - [ ] Click en un proceso navega al workspace correcto - [ ] Migración SQL ejecutada sin errores; `SELECT company FROM users` no falla - [ ] AppHeader muestra empresa si está definida (probar editando desde el Sheet) - [ ] Sheet de perfil: guardar nombre y empresa persiste en Supabase y se refleja en el dropdown - [ ] Email en el Sheet es solo lectura - [ ] 528+ tests Vitest verdes - [ ] **Validación visual del director** antes del OK formal