# Sprint 4 — Etapa 6: Identidad en UI — header con usuario + "actualizado por" ## Contexto InQ ROI — React 18 + Vite + TypeScript + TanStack Router + Zustand + shadcn/ui + Tailwind. Etapas 1–5B completadas: auth Google, todos los repos en Supabase, RLS granular, UI de visibilidad en biblioteca, fix de Web Locks. Esta etapa agrega presencia del usuario en la UI: 1. **AppHeader** con avatar, nombre y logout 2. **"Actualizado por [nombre]"** en cada tarjeta de proceso de la biblioteca No hay cambios en PDF, simulación, workspace ni base de datos. --- ## Estado actual | Elemento | Estado | |---|---| | `AppHeader.tsx` | Estático — solo logo + texto "InQ ROI". No usa `useAuth` | | Tarjeta de proceso | Tiene timestamp relativo (`updatedAt`) pero sin nombre de persona | | `updated_by` en `fromRow` | Se escribe en DB pero `fromRow` lo descarta — no llega al tipo `Process` | | Resolución UUID → nombre | No existe. `ownerId` es UUID crudo, nunca se resuelve a nombre | --- ## Alcance estricto — qué entra en esta etapa ### 1. AppHeader — usuario logueado + logout Modificar `src/components/AppHeader.tsx` para consumir `useAuth()` y mostrar en el extremo derecho del header: **Versión mínima:** - Nombre del usuario (`user.name`) y botón "Cerrar sesión" que llama `authService.signOut()` **Versión con wow (implementar esta):** - Avatar circular con la foto del usuario (`user.avatarUrl`) o iniciales si no hay foto, usando el color naranja InQ `#F59845` como fondo de fallback - Al hacer click en el avatar, despliega un pequeño dropdown con: - Nombre completo + email del usuario - Opción "Cerrar sesión" con ícono `LogOut` de Lucide El dropdown debe cerrarse al hacer click fuera. Usar el componente `DropdownMenu` de shadcn/ui. **Restricción de marca:** el header usa el gradiente `#F59845 → #ED3E8F` como fondo. El dropdown debe tener fondo blanco/neutro (no gradiente). El texto del nombre en el header (si se muestra fuera del dropdown) va en blanco. --- ### 2. Tipo `Process` — agregar `updatedBy` + resolver nombres en query #### a) Actualizar `src/domain/types.ts` Agregar al final de la interfaz `Process`: ```typescript export interface Process { // ... campos existentes sin cambios ... updatedBy: string // 🆕 Sprint 4 — UUID del usuario que hizo el último update } ``` #### b) Actualizar `src/persistence/supabase/process-repo.ts` **En `fromRow`:** agregar mapeo del campo: ```typescript updatedBy: (row.updated_by as string) ?? '', ``` **En `getAll()`:** hacer un JOIN con la tabla `users` para traer nombres en una sola query (evitar N+1): ```typescript async getAll(): Promise { await supabase.auth.getSession() // barrera de sincronización (fix Etapa 5B) const { data, error } = await supabase .from('processes') .select(` *, owner:users!owner_id(id, name, avatar_url), updater:users!updated_by(id, name) `) .order('updated_at', { ascending: false }) if (error) throw error return (data ?? []).map(fromRowWithUsers) } ``` #### c) Tipo extendido para display — NO contaminar el dominio Definir en `src/persistence/supabase/process-repo.ts` (no en `domain/types.ts`): ```typescript // Solo para display — no es parte del dominio puro export interface ProcessWithUserNames extends Process { ownerName: string ownerAvatarUrl?: string updaterName: string } ``` El dominio `Process` mantiene solo `ownerId` y `updatedBy` (UUIDs). La resolución a nombres es responsabilidad del repositorio, no del dominio. **`fromRowWithUsers`**: función nueva que mapea el resultado del JOIN: ```typescript function fromRowWithUsers(row: ProcessRowWithJoins): ProcessWithUserNames { return { ...fromRow(row), ownerName: (row.owner as { name: string } | null)?.name ?? 'Desconocido', ownerAvatarUrl: (row.owner as { avatar_url: string } | null)?.avatar_url ?? undefined, updaterName: (row.updater as { name: string } | null)?.name ?? 'Desconocido', } } ``` Si el JOIN falla por alguna razón (usuario eliminado, campo nulo), el fallback es `'Desconocido'` — no crashear. --- ### 3. LibraryPage — "Actualizado por [nombre]" en tarjetas En `src/features/library/LibraryPage.tsx`: **Cambiar** el store/hook que provee los procesos para que use `ProcessWithUserNames` en lugar de `Process` — el tipo ya viene resuelto del repositorio. **En la `ProcessCard`**, reemplazar la línea actual: ```tsx "Modificado {relativeDate(process.updatedAt)}" ``` Por: ```tsx "Actualizado por {process.updaterName} · {relativeDate(process.updatedAt)}" ``` **Versión con wow (implementar esta):** - Si el `updaterName` coincide con el usuario actual (`updaterName === currentUser.name`), mostrar "por vos" en lugar del nombre - Tooltip al pasar por encima que muestra la fecha completa (no solo relativa) --- ### 4. Workspace — "Actualizado por" en header del proceso (opcional, si cabe en scope) Si la estructura del workspace ya tiene un lugar para metadatos del proceso (nombre del proceso, cliente), agregar debajo del nombre: ``` Actualizado por [nombre] · hace 2 días ``` Si esto requiere cambios en el store o en cómo WorkspacePage carga el proceso, diferirlo — no es bloqueante para el sprint. --- ## Qué NO entra en esta etapa - Panel de perfil de usuario (editar nombre, foto) - Gestión de grupos de usuarios - ResourcesPanel: UX de catálogo global → Etapa 7 - Cambios en PDF - Cambios en simulación - Tests de integración de auth → Etapa 8 --- ## Orden de ejecución 1. Actualizar `src/domain/types.ts` — agregar `updatedBy: string` a `Process` 2. Actualizar `src/persistence/supabase/process-repo.ts`: - `fromRow`: mapear `updated_by` - Agregar `ProcessWithUserNames` interface - Agregar `fromRowWithUsers` function - Modificar `getAll()` para usar JOIN 3. Actualizar el store/hook de biblioteca para propagar `ProcessWithUserNames` 4. Modificar `ProcessCard` en `LibraryPage.tsx` — "Actualizado por [nombre]" 5. Modificar `AppHeader.tsx` — avatar + dropdown con logout 6. (Opcional) "Actualizado por" en workspace si es < 30 min de trabajo 7. `npm run build` — debe pasar sin errores TypeScript 8. `npm run test` — debe mantener 521+ tests verdes 9. Commit: `feat(sprint-4/etapa-6): identidad en UI — header con usuario + actualizado por` --- ## Criterios de validación - [ ] AppHeader muestra avatar del usuario logueado (o iniciales con fondo naranja si no hay foto) - [ ] Click en avatar despliega dropdown con nombre, email y "Cerrar sesión" - [ ] "Cerrar sesión" funciona — redirige a `/login` - [ ] Tarjetas de proceso muestran "Actualizado por [nombre]" - [ ] Si el updater es el usuario actual, muestra "por vos" - [ ] El JOIN en `getAll()` no rompe el build (TypeScript sin `any` injustificado) - [ ] `Process.updatedBy` existe en `types.ts` - [ ] `fromRow` mapea `updated_by` - [ ] `npm run build` limpio - [ ] 521+ tests verdes - [ ] **Validación visual del director** — ver avatar en header y nombres en tarjetas antes del OK formal --- ## Nota sobre el JOIN de Supabase El JOIN `users!owner_id(...)` usa la sintaxis de PostgREST para FK explícita. Si la FK no está declarada en el schema de Supabase (aunque exista en PostgreSQL), el join puede fallar. Verificar en Supabase Dashboard → Table Editor → processes que las FK `owner_id → users.id` y `updated_by → users.id` aparecen como relaciones. Si no aparecen, agregar al schema o usar un select con `.eq()` fallback. Alternativa si el JOIN falla: hacer `getAll()` + un `supabase.from('users').select('id, name, avatar_url')` separado y mapear en memoria. Menos elegante pero más robusto. Preferir el JOIN pero tener este fallback documentado.