From 3a534f7ad3d1d2fe257671cb6c3ddd5bbbd4e873 Mon Sep 17 00:00:00 2001 From: dmolinari Date: Tue, 21 Apr 2026 13:37:53 -0300 Subject: [PATCH] chore(frontend): reorganize sidebar into grouped sections + remove disabled items MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Problem: sidebar was growing unwieldy — 4 top-level disabled items marked 'Próx.' acted as visual noise, and 12 admin items sat in a flat list with no grouping (hard to scan). Changes: - Remove the 4 disabled top-level items (Ventas, Tasación, Integraciones, Administración-as-link). Those features will surface via the admin subsections when actually implemented, not as placeholder ghosts. - Group the 12 admin items into 4 domain-aligned sections: - Seguridad: Usuarios, Crear Usuario, Roles, Permisos, Auditoría - Maestros: Medios, Secciones, Puntos de Venta - Catálogo: Rubros, Tipos de Producto, Productos - Tasación: Caracteres Tasables - Sections auto-hide when no item passes the permission filter, preventing empty headers for users with limited roles. - Dashboard remains as the single top-level nav item (always visible). TDD: new AppSidebar.test.tsx covers 10 scenarios — section rendering, permission filtering, section auto-hide, role gating, active-route marking, and section ordering. --- src/web/src/components/layout/AppSidebar.tsx | 233 +++++++++--------- .../layout/__tests__/AppSidebar.test.tsx | 161 ++++++++++++ 2 files changed, 276 insertions(+), 118 deletions(-) create mode 100644 src/web/src/components/layout/__tests__/AppSidebar.test.tsx diff --git a/src/web/src/components/layout/AppSidebar.tsx b/src/web/src/components/layout/AppSidebar.tsx index 785aeb7..1f61acd 100644 --- a/src/web/src/components/layout/AppSidebar.tsx +++ b/src/web/src/components/layout/AppSidebar.tsx @@ -1,12 +1,8 @@ import { Link, useLocation } from 'react-router-dom' import { LayoutDashboard, - ShoppingCart, - Calculator, - Zap, - Settings, - UserPlus, Users, + UserPlus, ShieldCheck, KeyRound, FileClock, @@ -21,7 +17,6 @@ import { Hash, } from 'lucide-react' import { cn } from '@/lib/utils' -import { Badge } from '@/components/ui/badge' import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' import { useAuthStore } from '@/stores/authStore' import { useSidebar } from '@/hooks/useSidebar' @@ -30,71 +25,103 @@ interface NavItem { label: string href: string icon: React.ElementType - disabled?: boolean /** Si se define, el item solo se muestra si el user tiene este permiso. */ requiredPermission?: string } -const navItems: NavItem[] = [ - { label: 'Dashboard', href: '/', icon: LayoutDashboard }, - { label: 'Ventas', href: '/ventas', icon: ShoppingCart, disabled: true }, - { label: 'Tasación', href: '/tasacion', icon: Calculator, disabled: true }, - { label: 'Integraciones', href: '/integraciones', icon: Zap, disabled: true }, - { label: 'Administración', href: '/administracion', icon: Settings, disabled: true }, -] +interface NavSection { + label: string + /** Si true, la sección solo se muestra si el user tiene rol admin. */ + adminOnly?: boolean + items: NavItem[] +} -const adminItems: NavItem[] = [ - { label: 'Usuarios', href: '/usuarios', icon: Users }, - { label: 'Crear Usuario', href: '/usuarios/nuevo', icon: UserPlus }, - { label: 'Roles', href: '/admin/roles', icon: ShieldCheck }, - { label: 'Permisos', href: '/admin/permisos', icon: KeyRound }, +// Item principal — siempre visible para usuarios autenticados +const dashboardItem: NavItem = { + label: 'Dashboard', + href: '/', + icon: LayoutDashboard, +} + +// Secciones de navegación agrupadas por dominio. +// Orden: Seguridad → Maestros → Catálogo → Tasación (coincide con la arquitectura del proyecto). +// Cada sección se oculta si todos sus items están filtrados por permisos. +const navSections: NavSection[] = [ { - label: 'Auditoría', - href: '/admin/audit', - icon: FileClock, - requiredPermission: 'administracion:auditoria:ver', + label: 'Seguridad', + adminOnly: true, + items: [ + { label: 'Usuarios', href: '/usuarios', icon: Users }, + { label: 'Crear Usuario', href: '/usuarios/nuevo', icon: UserPlus }, + { label: 'Roles', href: '/admin/roles', icon: ShieldCheck }, + { label: 'Permisos', href: '/admin/permisos', icon: KeyRound }, + { + label: 'Auditoría', + href: '/admin/audit', + icon: FileClock, + requiredPermission: 'administracion:auditoria:ver', + }, + ], }, { - label: 'Medios', - href: '/admin/medios', - icon: Newspaper, - requiredPermission: 'administracion:medios:gestionar', + label: 'Maestros', + adminOnly: true, + items: [ + { + label: 'Medios', + href: '/admin/medios', + icon: Newspaper, + requiredPermission: 'administracion:medios:gestionar', + }, + { + label: 'Secciones', + href: '/admin/secciones', + icon: Columns3, + requiredPermission: 'administracion:secciones:gestionar', + }, + { + label: 'Puntos de Venta', + href: '/admin/puntos-de-venta', + icon: Store, + requiredPermission: 'administracion:puntos_de_venta:gestionar', + }, + ], }, { - label: 'Secciones', - href: '/admin/secciones', - icon: Columns3, - requiredPermission: 'administracion:secciones:gestionar', + label: 'Catálogo', + adminOnly: true, + items: [ + { + label: 'Rubros', + href: '/admin/rubros', + icon: Tag, + requiredPermission: 'catalogo:rubros:gestionar', + }, + { + label: 'Tipos de Producto', + href: '/admin/product-types', + icon: Layers, + requiredPermission: 'catalogo:tipos:gestionar', + }, + { + label: 'Productos', + href: '/admin/products', + icon: Package, + requiredPermission: 'catalogo:productos:gestionar', + }, + ], }, { - label: 'Puntos de Venta', - href: '/admin/puntos-de-venta', - icon: Store, - requiredPermission: 'administracion:puntos_de_venta:gestionar', - }, - { - label: 'Rubros', - href: '/admin/rubros', - icon: Tag, - requiredPermission: 'catalogo:rubros:gestionar', - }, - { - label: 'Tipos de Producto', - href: '/admin/product-types', - icon: Layers, - requiredPermission: 'catalogo:tipos:gestionar', - }, - { - label: 'Productos', - href: '/admin/products', - icon: Package, - requiredPermission: 'catalogo:productos:gestionar', - }, - { - label: 'Caracteres Tasables', - href: '/admin/tasacion/chargeable-chars', - icon: Hash, - requiredPermission: 'tasacion:caracteres_especiales:gestionar', + label: 'Tasación', + adminOnly: true, + items: [ + { + label: 'Caracteres Tasables', + href: '/admin/tasacion/chargeable-chars', + icon: Hash, + requiredPermission: 'tasacion:caracteres_especiales:gestionar', + }, + ], }, ] @@ -111,7 +138,6 @@ export function SidebarNav({ forceExpanded = false }: SidebarNavProps) { const collapsed = forceExpanded ? false : persisted function isItemActive(item: NavItem): boolean { - if (item.disabled) return false if (item.href === '/') return pathname === '/' if (item.href === '/usuarios') { return pathname.startsWith('/usuarios') && pathname !== '/usuarios/nuevo' @@ -120,6 +146,20 @@ export function SidebarNav({ forceExpanded = false }: SidebarNavProps) { return pathname.startsWith(item.href) } + function hasAccess(item: NavItem): boolean { + return !item.requiredPermission || (user?.permisos.includes(item.requiredPermission) ?? false) + } + + // Computa las secciones visibles (con items filtrados por permiso). + // Si una sección queda sin items tras el filtro, se oculta el header. + const visibleSections = navSections + .filter((section) => !section.adminOnly || isAdmin) + .map((section) => ({ + ...section, + items: section.items.filter(hasAccess), + })) + .filter((section) => section.items.length > 0) + return ( ) @@ -217,41 +250,6 @@ function NavRow({ item, collapsed, active }: NavRowProps) { collapsed ? 'justify-center h-10 w-10 mx-auto' : 'gap-3 px-3 py-2', ) - // Disabled item - if (item.disabled) { - const content = ( -
- - {!collapsed && ( - <> - {item.label} - - Próx. - - - )} -
- ) - - if (!collapsed) return content - return ( - - {content} - - {item.label}{' '} - · Próximamente - - - ) - } - - // Active link const link = ( - {/* Active indicator bar (left edge) when expanded */} {active && !collapsed && ( )} @@ -292,7 +289,7 @@ function SectionLabel({ return