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