556 lines
22 KiB
TypeScript
556 lines
22 KiB
TypeScript
'use client'
|
|
|
|
import * as React from 'react'
|
|
import Link from 'next/link'
|
|
import { usePathname } from 'next/navigation'
|
|
import Image from 'next/image'
|
|
import { cn } from '@/lib/utils'
|
|
import { Button } from '@/components/ui/button'
|
|
import { ScrollArea } from '@/components/ui/scroll-area'
|
|
import { Avatar, AvatarFallback } from '@/components/ui/avatar'
|
|
import { Badge } from '@/components/ui/badge'
|
|
import {
|
|
Tooltip,
|
|
TooltipContent,
|
|
TooltipProvider,
|
|
TooltipTrigger,
|
|
} from '@/components/ui/tooltip'
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuLabel,
|
|
DropdownMenuSeparator,
|
|
DropdownMenuTrigger,
|
|
} from '@/components/ui/dropdown-menu'
|
|
import {
|
|
Sheet,
|
|
SheetContent,
|
|
SheetTrigger,
|
|
} from '@/components/ui/sheet'
|
|
import {
|
|
LayoutDashboard,
|
|
FileText,
|
|
ClipboardCheck,
|
|
Users,
|
|
BarChart3,
|
|
Settings,
|
|
Bell,
|
|
ChevronLeft,
|
|
ChevronRight,
|
|
LogOut,
|
|
User,
|
|
Building2,
|
|
PlusCircle,
|
|
Briefcase,
|
|
Clock,
|
|
AlertTriangle,
|
|
Menu,
|
|
} from 'lucide-react'
|
|
import { currentUser, notifications } from '@/lib/mock-data'
|
|
|
|
interface NavItem {
|
|
title: string
|
|
href: string
|
|
icon: React.ComponentType<{ className?: string }>
|
|
badge?: number
|
|
roles?: string[]
|
|
}
|
|
|
|
const mainNavItems: NavItem[] = [
|
|
{
|
|
title: 'Dashboard',
|
|
href: '/dashboard',
|
|
icon: LayoutDashboard,
|
|
},
|
|
{
|
|
title: 'Nueva Requisición',
|
|
href: '/requisiciones/nueva',
|
|
icon: PlusCircle,
|
|
},
|
|
{
|
|
title: 'Mis Requisiciones',
|
|
href: '/requisiciones',
|
|
icon: FileText,
|
|
},
|
|
{
|
|
title: 'Aprobaciones',
|
|
href: '/aprobaciones',
|
|
icon: ClipboardCheck,
|
|
badge: 2,
|
|
roles: ['rrhh_local', 'rrhh_regional', 'administracion', 'director_rrhh'],
|
|
},
|
|
{
|
|
title: 'Pipeline Reclutamiento',
|
|
href: '/pipeline',
|
|
icon: Briefcase,
|
|
roles: ['reclutador', 'director_rrhh', 'rrhh_regional'],
|
|
},
|
|
]
|
|
|
|
const secondaryNavItems: NavItem[] = [
|
|
{
|
|
title: 'Reclutadores',
|
|
href: '/reclutadores',
|
|
icon: Users,
|
|
roles: ['director_rrhh', 'rrhh_regional'],
|
|
},
|
|
{
|
|
title: 'Reportes',
|
|
href: '/reportes',
|
|
icon: BarChart3,
|
|
roles: ['director_rrhh', 'rrhh_regional', 'country_lead'],
|
|
},
|
|
{
|
|
title: 'Administración',
|
|
href: '/admin',
|
|
icon: Settings,
|
|
roles: ['admin_sistema', 'director_rrhh'],
|
|
},
|
|
]
|
|
|
|
function getInitials(name: string): string {
|
|
return name
|
|
.split(' ')
|
|
.map(n => n[0])
|
|
.join('')
|
|
.toUpperCase()
|
|
.slice(0, 2)
|
|
}
|
|
|
|
export function MainLayout({ children }: { children: React.ReactNode }) {
|
|
const pathname = usePathname()
|
|
const [collapsed, setCollapsed] = React.useState(false)
|
|
const unreadNotifications = notifications.filter(n => !n.read).length
|
|
|
|
const userRole = currentUser.role
|
|
|
|
const filterByRole = (items: NavItem[]) => {
|
|
return items.filter(item => {
|
|
if (!item.roles) return true
|
|
return item.roles.includes(userRole)
|
|
})
|
|
}
|
|
|
|
const filteredMainNav = filterByRole(mainNavItems)
|
|
const filteredSecondaryNav = filterByRole(secondaryNavItems)
|
|
|
|
return (
|
|
<TooltipProvider delayDuration={0}>
|
|
<div className="flex h-screen overflow-hidden bg-background">
|
|
{/* Desktop Sidebar */}
|
|
<aside
|
|
className={cn(
|
|
'relative hidden md:flex flex-col border-r border-sidebar-border bg-sidebar transition-all duration-300',
|
|
collapsed ? 'w-[70px]' : 'w-[260px]'
|
|
)}
|
|
>
|
|
{/* Logo */}
|
|
<div className={cn(
|
|
'flex h-16 items-center border-b border-sidebar-border px-4',
|
|
collapsed ? 'justify-center' : 'justify-start gap-3'
|
|
)}>
|
|
<div className="relative h-8 w-8 shrink-0 overflow-hidden rounded">
|
|
<Image
|
|
src="/images/logo-glm.png"
|
|
alt="GomezLee Marketing"
|
|
fill
|
|
className="object-contain"
|
|
/>
|
|
</div>
|
|
{!collapsed && (
|
|
<div className="flex flex-col">
|
|
<span className="text-sm font-semibold text-sidebar-foreground">Portal GLM</span>
|
|
<span className="text-xs text-sidebar-foreground/60">Requisiciones</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Navigation */}
|
|
<ScrollArea className="flex-1 py-4">
|
|
<nav className="space-y-6 px-3">
|
|
{/* Main Nav */}
|
|
<div className="space-y-1">
|
|
{!collapsed && (
|
|
<p className="mb-2 px-3 text-xs font-medium uppercase tracking-wider text-sidebar-foreground/50">
|
|
Principal
|
|
</p>
|
|
)}
|
|
{filteredMainNav.map((item) => {
|
|
const isActive = item.href === '/dashboard' || item.href === '/requisiciones'
|
|
? pathname === item.href
|
|
: pathname === item.href || pathname.startsWith(item.href + '/')
|
|
const Icon = item.icon
|
|
|
|
const linkContent = (
|
|
<Link
|
|
href={item.href}
|
|
className={cn(
|
|
'flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium transition-colors',
|
|
isActive
|
|
? 'bg-sidebar-primary text-sidebar-primary-foreground'
|
|
: 'text-sidebar-foreground/70 hover:bg-sidebar-accent hover:text-sidebar-foreground',
|
|
collapsed && 'justify-center px-2'
|
|
)}
|
|
>
|
|
<Icon className={cn('h-5 w-5 shrink-0', isActive && 'text-sidebar-primary-foreground')} />
|
|
{!collapsed && (
|
|
<>
|
|
<span className="flex-1">{item.title}</span>
|
|
{item.badge && item.badge > 0 && (
|
|
<Badge variant="secondary" className="ml-auto h-5 min-w-5 justify-center bg-sidebar-primary/20 px-1.5 text-xs text-sidebar-primary">
|
|
{item.badge}
|
|
</Badge>
|
|
)}
|
|
</>
|
|
)}
|
|
</Link>
|
|
)
|
|
|
|
if (collapsed) {
|
|
return (
|
|
<Tooltip key={item.href}>
|
|
<TooltipTrigger asChild>{linkContent}</TooltipTrigger>
|
|
<TooltipContent side="right" className="flex items-center gap-2">
|
|
{item.title}
|
|
{item.badge && item.badge > 0 && (
|
|
<Badge variant="secondary" className="h-5 min-w-5 justify-center px-1.5 text-xs">
|
|
{item.badge}
|
|
</Badge>
|
|
)}
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
)
|
|
}
|
|
|
|
return <React.Fragment key={item.href}>{linkContent}</React.Fragment>
|
|
})}
|
|
</div>
|
|
|
|
{/* Secondary Nav */}
|
|
{filteredSecondaryNav.length > 0 && (
|
|
<div className="space-y-1">
|
|
{!collapsed && (
|
|
<p className="mb-2 px-3 text-xs font-medium uppercase tracking-wider text-sidebar-foreground/50">
|
|
Gestión
|
|
</p>
|
|
)}
|
|
{filteredSecondaryNav.map((item) => {
|
|
const isActive = pathname === item.href || pathname.startsWith(item.href + '/')
|
|
const Icon = item.icon
|
|
|
|
const linkContent = (
|
|
<Link
|
|
href={item.href}
|
|
className={cn(
|
|
'flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium transition-colors',
|
|
isActive
|
|
? 'bg-sidebar-primary text-sidebar-primary-foreground'
|
|
: 'text-sidebar-foreground/70 hover:bg-sidebar-accent hover:text-sidebar-foreground',
|
|
collapsed && 'justify-center px-2'
|
|
)}
|
|
>
|
|
<Icon className={cn('h-5 w-5 shrink-0', isActive && 'text-sidebar-primary-foreground')} />
|
|
{!collapsed && <span>{item.title}</span>}
|
|
</Link>
|
|
)
|
|
|
|
if (collapsed) {
|
|
return (
|
|
<Tooltip key={item.href}>
|
|
<TooltipTrigger asChild>{linkContent}</TooltipTrigger>
|
|
<TooltipContent side="right">{item.title}</TooltipContent>
|
|
</Tooltip>
|
|
)
|
|
}
|
|
|
|
return <React.Fragment key={item.href}>{linkContent}</React.Fragment>
|
|
})}
|
|
</div>
|
|
)}
|
|
</nav>
|
|
</ScrollArea>
|
|
|
|
{/* User Section */}
|
|
<div className="border-t border-sidebar-border p-3">
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button
|
|
variant="ghost"
|
|
className={cn(
|
|
'w-full justify-start gap-3 px-3 py-2.5 text-sidebar-foreground hover:bg-sidebar-accent',
|
|
collapsed && 'justify-center px-2'
|
|
)}
|
|
>
|
|
<Avatar className="h-8 w-8 shrink-0">
|
|
<AvatarFallback className="bg-sidebar-primary text-sidebar-primary-foreground text-xs">
|
|
{getInitials(currentUser.name)}
|
|
</AvatarFallback>
|
|
</Avatar>
|
|
{!collapsed && (
|
|
<div className="flex flex-col items-start text-left">
|
|
<span className="text-sm font-medium">{currentUser.name}</span>
|
|
<span className="text-xs text-sidebar-foreground/60">Director RR.HH.</span>
|
|
</div>
|
|
)}
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="end" className="w-56">
|
|
<DropdownMenuLabel>Mi Cuenta</DropdownMenuLabel>
|
|
<DropdownMenuSeparator />
|
|
<DropdownMenuItem>
|
|
<User className="mr-2 h-4 w-4" />
|
|
Perfil
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem>
|
|
<Building2 className="mr-2 h-4 w-4" />
|
|
Configuración
|
|
</DropdownMenuItem>
|
|
<DropdownMenuSeparator />
|
|
<DropdownMenuItem className="text-destructive">
|
|
<LogOut className="mr-2 h-4 w-4" />
|
|
Cerrar Sesión
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
</div>
|
|
|
|
{/* Collapse Toggle */}
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
onClick={() => setCollapsed(!collapsed)}
|
|
className="absolute -right-3 top-20 z-10 h-6 w-6 rounded-full border border-sidebar-border bg-sidebar text-sidebar-foreground shadow-sm hover:bg-sidebar-accent"
|
|
>
|
|
{collapsed ? (
|
|
<ChevronRight className="h-3 w-3" />
|
|
) : (
|
|
<ChevronLeft className="h-3 w-3" />
|
|
)}
|
|
</Button>
|
|
</aside>
|
|
|
|
{/* Main Content */}
|
|
<div className="flex flex-1 flex-col overflow-hidden">
|
|
{/* Top Header */}
|
|
<header className="flex h-16 items-center justify-between border-b bg-card px-4 md:px-6">
|
|
<div className="flex items-center gap-4">
|
|
<Sheet>
|
|
<SheetTrigger asChild>
|
|
<Button variant="ghost" size="icon" className="md:hidden">
|
|
<Menu className="h-5 w-5" />
|
|
<span className="sr-only">Toggle menu</span>
|
|
</Button>
|
|
</SheetTrigger>
|
|
<SheetContent side="left" className="w-[260px] p-0 flex flex-col bg-sidebar border-r border-sidebar-border">
|
|
{/* Logo */}
|
|
<div className="flex h-16 items-center justify-start gap-3 border-b border-sidebar-border px-4">
|
|
<div className="relative h-8 w-8 shrink-0 overflow-hidden rounded">
|
|
<Image
|
|
src="/images/logo-glm.png"
|
|
alt="GomezLee Marketing"
|
|
fill
|
|
className="object-contain"
|
|
/>
|
|
</div>
|
|
<div className="flex flex-col">
|
|
<span className="text-sm font-semibold text-sidebar-foreground">Portal GLM</span>
|
|
<span className="text-xs text-sidebar-foreground/60">Requisiciones</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Navigation */}
|
|
<ScrollArea className="flex-1 py-4">
|
|
<nav className="space-y-6 px-3">
|
|
<div className="space-y-1">
|
|
<p className="mb-2 px-3 text-xs font-medium uppercase tracking-wider text-sidebar-foreground/50">
|
|
Principal
|
|
</p>
|
|
{filteredMainNav.map((item) => {
|
|
const isActive = item.href === '/dashboard' || item.href === '/requisiciones'
|
|
? pathname === item.href
|
|
: pathname === item.href || pathname.startsWith(item.href + '/')
|
|
const Icon = item.icon
|
|
return (
|
|
<Link
|
|
key={item.href}
|
|
href={item.href}
|
|
className={cn(
|
|
'flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium transition-colors',
|
|
isActive
|
|
? 'bg-sidebar-primary text-sidebar-primary-foreground'
|
|
: 'text-sidebar-foreground/70 hover:bg-sidebar-accent hover:text-sidebar-foreground'
|
|
)}
|
|
>
|
|
<Icon className={cn('h-5 w-5 shrink-0', isActive && 'text-sidebar-primary-foreground')} />
|
|
<span className="flex-1">{item.title}</span>
|
|
{item.badge && item.badge > 0 && (
|
|
<Badge variant="secondary" className="ml-auto h-5 min-w-5 justify-center bg-sidebar-primary/20 px-1.5 text-xs text-sidebar-primary">
|
|
{item.badge}
|
|
</Badge>
|
|
)}
|
|
</Link>
|
|
)
|
|
})}
|
|
</div>
|
|
|
|
{filteredSecondaryNav.length > 0 && (
|
|
<div className="space-y-1">
|
|
<p className="mb-2 px-3 text-xs font-medium uppercase tracking-wider text-sidebar-foreground/50">
|
|
Gestión
|
|
</p>
|
|
{filteredSecondaryNav.map((item) => {
|
|
const isActive = pathname === item.href || pathname.startsWith(item.href + '/')
|
|
const Icon = item.icon
|
|
return (
|
|
<Link
|
|
key={item.href}
|
|
href={item.href}
|
|
className={cn(
|
|
'flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium transition-colors',
|
|
isActive
|
|
? 'bg-sidebar-primary text-sidebar-primary-foreground'
|
|
: 'text-sidebar-foreground/70 hover:bg-sidebar-accent hover:text-sidebar-foreground'
|
|
)}
|
|
>
|
|
<Icon className={cn('h-5 w-5 shrink-0', isActive && 'text-sidebar-primary-foreground')} />
|
|
<span>{item.title}</span>
|
|
</Link>
|
|
)
|
|
})}
|
|
</div>
|
|
)}
|
|
</nav>
|
|
</ScrollArea>
|
|
|
|
{/* User Section */}
|
|
<div className="border-t border-sidebar-border p-3">
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button
|
|
variant="ghost"
|
|
className="w-full justify-start gap-3 px-3 py-2.5 text-sidebar-foreground hover:bg-sidebar-accent"
|
|
>
|
|
<Avatar className="h-8 w-8 shrink-0">
|
|
<AvatarFallback className="bg-sidebar-primary text-sidebar-primary-foreground text-xs">
|
|
{getInitials(currentUser.name)}
|
|
</AvatarFallback>
|
|
</Avatar>
|
|
<div className="flex flex-col items-start text-left">
|
|
<span className="text-sm font-medium">{currentUser.name}</span>
|
|
<span className="text-xs text-sidebar-foreground/60">Director RR.HH.</span>
|
|
</div>
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="end" className="w-56">
|
|
<DropdownMenuLabel>Mi Cuenta</DropdownMenuLabel>
|
|
<DropdownMenuSeparator />
|
|
<DropdownMenuItem>
|
|
<User className="mr-2 h-4 w-4" />
|
|
Perfil
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem>
|
|
<Building2 className="mr-2 h-4 w-4" />
|
|
Configuración
|
|
</DropdownMenuItem>
|
|
<DropdownMenuSeparator />
|
|
<DropdownMenuItem className="text-destructive">
|
|
<LogOut className="mr-2 h-4 w-4" />
|
|
Cerrar Sesión
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
</div>
|
|
</SheetContent>
|
|
</Sheet>
|
|
|
|
<div className="hidden sm:flex items-center gap-2 text-sm text-muted-foreground">
|
|
<Clock className="h-4 w-4" />
|
|
<span>{new Date().toLocaleDateString('es-ES', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' })}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-2">
|
|
{/* Quick Stats */}
|
|
<div className="mr-4 hidden items-center gap-4 md:flex">
|
|
<div className="flex items-center gap-2 text-sm">
|
|
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-amber-100 dark:bg-amber-900/30">
|
|
<Clock className="h-4 w-4 text-amber-600 dark:text-amber-400" />
|
|
</div>
|
|
<div>
|
|
<span className="font-medium">2</span>
|
|
<span className="text-muted-foreground ml-1">Pendientes</span>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center gap-2 text-sm">
|
|
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-red-100 dark:bg-red-900/30">
|
|
<AlertTriangle className="h-4 w-4 text-red-600 dark:text-red-400" />
|
|
</div>
|
|
<div>
|
|
<span className="font-medium">3</span>
|
|
<span className="text-muted-foreground ml-1">En riesgo</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Notifications */}
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button variant="ghost" size="icon" className="relative">
|
|
<Bell className="h-5 w-5" />
|
|
{unreadNotifications > 0 && (
|
|
<span className="absolute -right-0.5 -top-0.5 flex h-4 w-4 items-center justify-center rounded-full bg-destructive text-[10px] font-medium text-destructive-foreground">
|
|
{unreadNotifications}
|
|
</span>
|
|
)}
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="end" className="w-80">
|
|
<DropdownMenuLabel className="flex items-center justify-between">
|
|
Notificaciones
|
|
{unreadNotifications > 0 && (
|
|
<Badge variant="secondary" className="ml-2">
|
|
{unreadNotifications} nuevas
|
|
</Badge>
|
|
)}
|
|
</DropdownMenuLabel>
|
|
<DropdownMenuSeparator />
|
|
<ScrollArea className="h-[300px]">
|
|
{notifications.slice(0, 5).map((notif) => (
|
|
<DropdownMenuItem key={notif.id} className="flex flex-col items-start gap-1 p-3">
|
|
<div className="flex w-full items-start justify-between gap-2">
|
|
<span className={cn('text-sm font-medium', !notif.read && 'text-primary')}>
|
|
{notif.title}
|
|
</span>
|
|
{!notif.read && (
|
|
<span className="h-2 w-2 shrink-0 rounded-full bg-primary" />
|
|
)}
|
|
</div>
|
|
<span className="text-xs text-muted-foreground line-clamp-2">
|
|
{notif.message}
|
|
</span>
|
|
<span className="text-xs text-muted-foreground/60">
|
|
{new Date(notif.createdAt).toLocaleDateString('es-ES')}
|
|
</span>
|
|
</DropdownMenuItem>
|
|
))}
|
|
</ScrollArea>
|
|
<DropdownMenuSeparator />
|
|
<DropdownMenuItem className="justify-center text-primary">
|
|
Ver todas las notificaciones
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
</div>
|
|
</header>
|
|
|
|
{/* Page Content */}
|
|
<main className="flex-1 overflow-auto p-4 md:p-6 lg:p-8">
|
|
{children}
|
|
</main>
|
|
</div>
|
|
</div>
|
|
</TooltipProvider>
|
|
)
|
|
}
|