|
|
|
|
@@ -9,10 +9,14 @@ import {
|
|
|
|
|
Users,
|
|
|
|
|
ShieldCheck,
|
|
|
|
|
KeyRound,
|
|
|
|
|
PanelLeftClose,
|
|
|
|
|
PanelLeftOpen,
|
|
|
|
|
Newspaper,
|
|
|
|
|
} from 'lucide-react'
|
|
|
|
|
import { cn } from '@/lib/utils'
|
|
|
|
|
import { Badge } from '@/components/ui/badge'
|
|
|
|
|
import { useAuthStore } from '@/stores/authStore'
|
|
|
|
|
import { useSidebar } from '@/hooks/useSidebar'
|
|
|
|
|
|
|
|
|
|
interface NavItem {
|
|
|
|
|
label: string
|
|
|
|
|
@@ -25,127 +29,216 @@ 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,
|
|
|
|
|
},
|
|
|
|
|
{ label: 'Integraciones', href: '/integraciones', icon: Zap, disabled: true },
|
|
|
|
|
{ label: 'Administración', href: '/administracion', icon: Settings, disabled: true },
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
export function SidebarNav() {
|
|
|
|
|
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 },
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
interface SidebarNavProps {
|
|
|
|
|
/** When true forces expanded layout regardless of persisted state — used by mobile Sheet. */
|
|
|
|
|
forceExpanded?: boolean
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function SidebarNav({ forceExpanded = false }: SidebarNavProps) {
|
|
|
|
|
const { pathname } = useLocation()
|
|
|
|
|
const user = useAuthStore((s) => s.user)
|
|
|
|
|
const isAdmin = user?.rol === 'admin'
|
|
|
|
|
const { collapsed: persisted, toggle } = useSidebar()
|
|
|
|
|
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'
|
|
|
|
|
}
|
|
|
|
|
if (item.href === '/usuarios/nuevo') return pathname === '/usuarios/nuevo'
|
|
|
|
|
return pathname.startsWith(item.href)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<aside className="flex h-full flex-col bg-background border-r border-border">
|
|
|
|
|
<aside
|
|
|
|
|
className={cn(
|
|
|
|
|
'flex h-full flex-col bg-card text-card-foreground overflow-hidden transition-[width] duration-200 ease-out',
|
|
|
|
|
collapsed ? 'w-[68px]' : 'w-60',
|
|
|
|
|
)}
|
|
|
|
|
data-collapsed={collapsed}
|
|
|
|
|
>
|
|
|
|
|
{/* Brand */}
|
|
|
|
|
<div className="flex h-14 items-center px-4 border-b border-border">
|
|
|
|
|
<span className="text-base font-semibold tracking-tight text-foreground">
|
|
|
|
|
SIG-CM 2.0
|
|
|
|
|
</span>
|
|
|
|
|
<div className="flex h-14 items-center border-b border-border px-3 shrink-0">
|
|
|
|
|
<div className="flex h-9 w-9 items-center justify-center rounded-lg bg-gradient-to-br from-brand-500 to-violet-500 shadow-md shadow-brand-500/20 shrink-0">
|
|
|
|
|
<Newspaper className="h-4 w-4 text-white" strokeWidth={2.25} />
|
|
|
|
|
</div>
|
|
|
|
|
{!collapsed && (
|
|
|
|
|
<span className="ml-3 text-sm font-semibold tracking-tight text-foreground truncate">
|
|
|
|
|
SIG-CM 2.0
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Nav */}
|
|
|
|
|
<nav className="flex-1 overflow-y-auto py-4 px-2 space-y-1">
|
|
|
|
|
{navItems.map((item) => {
|
|
|
|
|
const Icon = item.icon
|
|
|
|
|
const isActive = pathname === item.href && !item.disabled
|
|
|
|
|
<nav className="flex-1 overflow-y-auto overflow-x-hidden py-3 px-2 space-y-1">
|
|
|
|
|
{navItems.map((item) => (
|
|
|
|
|
<NavRow
|
|
|
|
|
key={item.href}
|
|
|
|
|
item={item}
|
|
|
|
|
collapsed={collapsed}
|
|
|
|
|
active={isItemActive(item)}
|
|
|
|
|
/>
|
|
|
|
|
))}
|
|
|
|
|
|
|
|
|
|
return item.disabled ? (
|
|
|
|
|
<div
|
|
|
|
|
key={item.href}
|
|
|
|
|
className="flex items-center gap-3 rounded-md px-3 py-2 text-sm text-muted-foreground cursor-not-allowed opacity-60"
|
|
|
|
|
>
|
|
|
|
|
<Icon className="h-4 w-4 shrink-0" />
|
|
|
|
|
<span className="flex-1">{item.label}</span>
|
|
|
|
|
<Badge variant="secondary" className="text-xs">
|
|
|
|
|
Próximamente
|
|
|
|
|
</Badge>
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
<Link
|
|
|
|
|
key={item.href}
|
|
|
|
|
to={item.href}
|
|
|
|
|
className={cn(
|
|
|
|
|
'flex items-center gap-3 rounded-md px-3 py-2 text-sm transition-colors hover:bg-accent hover:text-accent-foreground',
|
|
|
|
|
isActive
|
|
|
|
|
? 'bg-accent text-accent-foreground font-medium'
|
|
|
|
|
: 'text-muted-foreground',
|
|
|
|
|
)}
|
|
|
|
|
>
|
|
|
|
|
<Icon className="h-4 w-4 shrink-0" />
|
|
|
|
|
<span>{item.label}</span>
|
|
|
|
|
</Link>
|
|
|
|
|
)
|
|
|
|
|
})}
|
|
|
|
|
|
|
|
|
|
{/* Admin-only section */}
|
|
|
|
|
{isAdmin && (
|
|
|
|
|
<>
|
|
|
|
|
<div className="pt-2 pb-1 px-3">
|
|
|
|
|
<span className="text-xs font-semibold uppercase tracking-wider text-muted-foreground/60">
|
|
|
|
|
Administración
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
<Link
|
|
|
|
|
to="/usuarios"
|
|
|
|
|
className={cn(
|
|
|
|
|
'flex items-center gap-3 rounded-md px-3 py-2 text-sm transition-colors hover:bg-accent hover:text-accent-foreground',
|
|
|
|
|
pathname.startsWith('/usuarios') && pathname !== '/usuarios/nuevo'
|
|
|
|
|
? 'bg-accent text-accent-foreground font-medium'
|
|
|
|
|
: 'text-muted-foreground',
|
|
|
|
|
)}
|
|
|
|
|
>
|
|
|
|
|
<Users className="h-4 w-4 shrink-0" />
|
|
|
|
|
<span>Usuarios</span>
|
|
|
|
|
</Link>
|
|
|
|
|
<Link
|
|
|
|
|
to="/usuarios/nuevo"
|
|
|
|
|
className={cn(
|
|
|
|
|
'flex items-center gap-3 rounded-md px-3 py-2 text-sm transition-colors hover:bg-accent hover:text-accent-foreground',
|
|
|
|
|
pathname === '/usuarios/nuevo'
|
|
|
|
|
? 'bg-accent text-accent-foreground font-medium'
|
|
|
|
|
: 'text-muted-foreground',
|
|
|
|
|
)}
|
|
|
|
|
>
|
|
|
|
|
<UserPlus className="h-4 w-4 shrink-0" />
|
|
|
|
|
<span>Crear Usuario</span>
|
|
|
|
|
</Link>
|
|
|
|
|
<Link
|
|
|
|
|
to="/admin/roles"
|
|
|
|
|
className={cn(
|
|
|
|
|
'flex items-center gap-3 rounded-md px-3 py-2 text-sm transition-colors hover:bg-accent hover:text-accent-foreground',
|
|
|
|
|
pathname.startsWith('/admin/roles')
|
|
|
|
|
? 'bg-accent text-accent-foreground font-medium'
|
|
|
|
|
: 'text-muted-foreground',
|
|
|
|
|
)}
|
|
|
|
|
>
|
|
|
|
|
<ShieldCheck className="h-4 w-4 shrink-0" />
|
|
|
|
|
<span>Roles</span>
|
|
|
|
|
</Link>
|
|
|
|
|
<Link
|
|
|
|
|
to="/admin/permisos"
|
|
|
|
|
className={cn(
|
|
|
|
|
'flex items-center gap-3 rounded-md px-3 py-2 text-sm transition-colors hover:bg-accent hover:text-accent-foreground',
|
|
|
|
|
pathname.startsWith('/admin/permisos')
|
|
|
|
|
? 'bg-accent text-accent-foreground font-medium'
|
|
|
|
|
: 'text-muted-foreground',
|
|
|
|
|
)}
|
|
|
|
|
>
|
|
|
|
|
<KeyRound className="h-4 w-4 shrink-0" />
|
|
|
|
|
<span>Permisos</span>
|
|
|
|
|
</Link>
|
|
|
|
|
<SectionLabel collapsed={collapsed}>Administración</SectionLabel>
|
|
|
|
|
{adminItems.map((item) => (
|
|
|
|
|
<NavRow
|
|
|
|
|
key={item.href}
|
|
|
|
|
item={item}
|
|
|
|
|
collapsed={collapsed}
|
|
|
|
|
active={isItemActive(item)}
|
|
|
|
|
/>
|
|
|
|
|
))}
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
</nav>
|
|
|
|
|
|
|
|
|
|
{/* Collapse toggle — visible only on desktop (no en Sheet) */}
|
|
|
|
|
{!forceExpanded && (
|
|
|
|
|
<div className="border-t border-border p-2 shrink-0">
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={toggle}
|
|
|
|
|
aria-label={collapsed ? 'Expandir sidebar' : 'Colapsar sidebar'}
|
|
|
|
|
className={cn(
|
|
|
|
|
'group/toggle relative w-full flex items-center gap-3 rounded-md px-3 py-2 text-sm transition-colors',
|
|
|
|
|
'text-muted-foreground hover:bg-accent hover:text-accent-foreground',
|
|
|
|
|
collapsed && 'justify-center',
|
|
|
|
|
)}
|
|
|
|
|
>
|
|
|
|
|
{collapsed ? (
|
|
|
|
|
<PanelLeftOpen className="h-4 w-4 shrink-0" />
|
|
|
|
|
) : (
|
|
|
|
|
<PanelLeftClose className="h-4 w-4 shrink-0" />
|
|
|
|
|
)}
|
|
|
|
|
{!collapsed && <span>Colapsar</span>}
|
|
|
|
|
{collapsed && (
|
|
|
|
|
<SidebarTooltip>Expandir</SidebarTooltip>
|
|
|
|
|
)}
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</aside>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ────────────────────────────────────────────────────────────────
|
|
|
|
|
Sub-componentes
|
|
|
|
|
──────────────────────────────────────────────────────────────── */
|
|
|
|
|
|
|
|
|
|
interface NavRowProps {
|
|
|
|
|
item: NavItem
|
|
|
|
|
collapsed: boolean
|
|
|
|
|
active: boolean
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function NavRow({ item, collapsed, active }: NavRowProps) {
|
|
|
|
|
const Icon = item.icon
|
|
|
|
|
|
|
|
|
|
const baseClasses = cn(
|
|
|
|
|
'group/item relative flex items-center rounded-md text-sm transition-colors',
|
|
|
|
|
collapsed ? 'justify-center h-10 px-0' : 'gap-3 px-3 py-2',
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if (item.disabled) {
|
|
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
className={cn(
|
|
|
|
|
baseClasses,
|
|
|
|
|
'text-muted-foreground/70 cursor-not-allowed opacity-60',
|
|
|
|
|
)}
|
|
|
|
|
aria-disabled="true"
|
|
|
|
|
>
|
|
|
|
|
<Icon className="h-4 w-4 shrink-0" />
|
|
|
|
|
{!collapsed && (
|
|
|
|
|
<>
|
|
|
|
|
<span className="flex-1 truncate">{item.label}</span>
|
|
|
|
|
<Badge variant="secondary" className="text-[10px] px-1.5 py-0 shrink-0">
|
|
|
|
|
Próx.
|
|
|
|
|
</Badge>
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
{collapsed && <SidebarTooltip>{item.label} · Próximamente</SidebarTooltip>}
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<Link
|
|
|
|
|
to={item.href}
|
|
|
|
|
className={cn(
|
|
|
|
|
baseClasses,
|
|
|
|
|
active
|
|
|
|
|
? 'bg-accent text-accent-foreground font-medium'
|
|
|
|
|
: 'text-muted-foreground hover:bg-accent hover:text-accent-foreground',
|
|
|
|
|
)}
|
|
|
|
|
aria-current={active ? 'page' : undefined}
|
|
|
|
|
>
|
|
|
|
|
{/* Active indicator bar (left edge) — sutil cuando expanded, dot cuando collapsed */}
|
|
|
|
|
{active && !collapsed && (
|
|
|
|
|
<span className="absolute left-0 top-1/2 -translate-y-1/2 h-5 w-0.5 rounded-r-full bg-primary" />
|
|
|
|
|
)}
|
|
|
|
|
<Icon className="h-4 w-4 shrink-0" />
|
|
|
|
|
{!collapsed && <span className="truncate">{item.label}</span>}
|
|
|
|
|
{collapsed && <SidebarTooltip>{item.label}</SidebarTooltip>}
|
|
|
|
|
</Link>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function SectionLabel({
|
|
|
|
|
collapsed,
|
|
|
|
|
children,
|
|
|
|
|
}: {
|
|
|
|
|
collapsed: boolean
|
|
|
|
|
children: React.ReactNode
|
|
|
|
|
}) {
|
|
|
|
|
if (collapsed) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="my-2 mx-2 border-t border-border" aria-hidden="true" />
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
return (
|
|
|
|
|
<div className="pt-3 pb-1 px-3">
|
|
|
|
|
<span className="text-[10px] font-semibold uppercase tracking-wider text-muted-foreground/60">
|
|
|
|
|
{children}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Custom tooltip que aparece a la derecha del item cuando el sidebar está collapsed.
|
|
|
|
|
* Sin radix dep — pure CSS hover + group selector. Se posiciona absolute fuera del sidebar.
|
|
|
|
|
*/
|
|
|
|
|
function SidebarTooltip({ children }: { children: React.ReactNode }) {
|
|
|
|
|
return (
|
|
|
|
|
<span
|
|
|
|
|
role="tooltip"
|
|
|
|
|
className={cn(
|
|
|
|
|
'pointer-events-none absolute left-full ml-3 top-1/2 -translate-y-1/2 z-50',
|
|
|
|
|
'whitespace-nowrap rounded-md border border-border bg-popover px-2.5 py-1.5',
|
|
|
|
|
'text-xs font-medium text-popover-foreground shadow-lg',
|
|
|
|
|
'opacity-0 -translate-x-1 transition-all duration-150',
|
|
|
|
|
'group-hover/item:opacity-100 group-hover/item:translate-x-0',
|
|
|
|
|
'group-hover/toggle:opacity-100 group-hover/toggle:translate-x-0',
|
|
|
|
|
)}
|
|
|
|
|
>
|
|
|
|
|
{children}
|
|
|
|
|
</span>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|