Files
portal-requisicion-vacantes/components/layout/main-layout.tsx
T
2026-06-12 16:08:49 -04:00

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>
)
}