Navegabilidad: Administración agrupada en 2 secciones con tabs
Date: 2026-07-25 Status: ✅ Integrado en router/index.ts + AppLayout.vue y verificado end-to-end (2026-07-27)
Qué cambia
El sidebar de "Administración" tenía 7 ítems sueltos. Se agrupan 5 de ellos en 2 secciones con tabs internas (Configuración y Auditoría quedan sueltas, sin cambios):
- Usuarios y Accesos (
/admin/usuarios-accesos) — tabs: Usuarios · Roles y Permisos · Asignación de Roles - Ubicaciones (
/admin/ubicaciones) — tabs: Países · Estaciones / PDV
Decisión de diseño clave — por qué tabs y no una página fusionada de verdad
Cada tab sigue siendo su propia ruta (/admin/usuarios-accesos/usuarios, /admin/usuarios-accesos/roles-permisos, etc.), no un v-if local dentro de un solo componente. Esto es intencional: Usuarios, Roles y Permisos, y Asignación de Roles tienen 3 filas independientes en la matriz de permisos L/E/B — con rutas separadas, el guard de permisos que ya existe (o el que se implemente) sigue funcionando por módulo sin tener que inventar lógica de "esconder esta pestaña si el usuario no tiene tal permiso". Si en algún momento se quiere ese comportamiento (ocultar tabs según permiso), es un agregado sobre esto, no un rediseño.
Componentes/rutas adjuntos (reference/)
TabbedSectionLayout.vue— layout genérico reutilizable: título, descripción opcional, barra de tabs (cada uno unrouter-linkreal), y un<router-view/>para el contenido de la pestaña activa. Ya tiene el estilo de marca (acento naranja en la tab activa) y dark mode.router-index.ts— el archivo completo de rutas, conadmin/usuarios-accesosyadmin/ubicacionesya anidados con sus hijos y elredirectde cada uno a la primera pestaña. Ojo: elredirecttiene que ser un path absoluto (/admin/usuarios-accesos/usuarios), no relativo (usuarios) — un redirect relativo en un route hijo con path vacío resuelve mal en Vue Router 4 (se comprobó en la validación: resolvía a/usuariosen la raíz en vez del path anidado correcto).AppLayout-nav-section.txt— el arraynavactualizado deAppLayout.vue(solo esa sección, no el archivo completo, para que sea fácil de mergear con lo que ya exista ahí).
Checklist de integración
- [x] Copiar
reference/TabbedSectionLayout.vueafrontend/src/layouts/. - [x] Mergear las rutas de
reference/router-index.ts(seccionesadmin/usuarios-accesosyadmin/ubicaciones) en el router real — se preservómeta: { module: '...' }en cada child route (el guard real usameta.module, nometa.permission— el nombre genérico del checklist original no coincidía 1:1 con la convención ya usada en el código real). - [x] Actualizar el array
navdeAppLayout.vuesegúnreference/AppLayout-nav-section.txt. La referencia no traía el campomodule(necesario para el filtro de visibilidad por permiso) ni usaba los íconos reales de@lucide/vue(traía paths SVG crudos) — adaptado: cada ítem agrupado ahora usamodules: string[](visible si el usuario puede Leer AL MENOS uno de los módulos del grupo) en vez demodule: stringúnico, con un helperisVisible()compartido entre el nav de escritorio y el de mobile. - [x] Confirmar que el ítem del sidebar ("Usuarios y Accesos", "Ubicaciones") queda resaltado activo en cualquiera de sus tabs — el código real ya tenía un
isActive(path)propio (agregado en el fix de sidebar dedoc/plans/2026-07-25-mejoras-listado-transacciones.md) que matchea por prefijo de path en vez delactive-classnativo de Vue Router. Confirmado que ya cubre este caso sin cambios adicionales. - [x] Guards de permiso por ruta: el proyecto ya usaba
meta: { module: '...' }(nometa: { permission: '...' }) — se agregó a cada child route individual, la ruta padre (TabbedSectionLayout) no llevameta.module.
Bug real encontrado en la integración (no estaba en el checklist original):firstAllowedRoute() en router/index.ts (fallback tras login o ruta bloqueada) iteraba router.getRoutes() asumiendo que ese orden coincidía con la prioridad del array candidates (dashboard primero). Dejó de ser cierto al anidar 5 rutas bajo los 2 layouts nuevos — Vue Router ordena getRoutes() por score de matching interno, no por prioridad de sitemap. Un Supervisor sin acceso a una tab bloqueada caía en Auditoría en vez de Dashboard. Corregido iterando candidates directamente (con un Map para el lookup por nombre) en vez de confiar en el orden de getRoutes().
Nota sobre Configuración y Auditoría
Quedan sueltas, sin agrupar — confirmado explícitamente con el cliente, no es un olvido.

