7.6 KiB
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:
- AppHeader con avatar, nombre y logout
- "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 llamaauthService.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#F59845como 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
LogOutde 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:
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:
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):
async getAll(): Promise<ProcessWithUserNames[]> {
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):
// 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:
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:
"Modificado {relativeDate(process.updatedAt)}"
Por:
"Actualizado por {process.updaterName} · {relativeDate(process.updatedAt)}"
Versión con wow (implementar esta):
- Si el
updaterNamecoincide 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
- Actualizar
src/domain/types.ts— agregarupdatedBy: stringaProcess - Actualizar
src/persistence/supabase/process-repo.ts:fromRow: mapearupdated_by- Agregar
ProcessWithUserNamesinterface - Agregar
fromRowWithUsersfunction - Modificar
getAll()para usar JOIN
- Actualizar el store/hook de biblioteca para propagar
ProcessWithUserNames - Modificar
ProcessCardenLibraryPage.tsx— "Actualizado por [nombre]" - Modificar
AppHeader.tsx— avatar + dropdown con logout - (Opcional) "Actualizado por" en workspace si es < 30 min de trabajo
npm run build— debe pasar sin errores TypeScriptnpm run test— debe mantener 521+ tests verdes- 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 sinanyinjustificado) Process.updatedByexiste entypes.tsfromRowmapeaupdated_bynpm run buildlimpio- 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.