Files
portal-requisicion-vacantes/components/pages/users-management-page.tsx
T
2026-06-12 16:08:49 -04:00

488 lines
20 KiB
TypeScript

"use client"
import { useState } from "react"
import { mockUsers, mockDepartments } from "@/lib/mock-data"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Badge } from "@/components/ui/badge"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"
import { Label } from "@/components/ui/label"
import { Switch } from "@/components/ui/switch"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
import {
Search,
Plus,
MoreVertical,
Mail,
Phone,
Shield,
User,
Users,
Building2,
Edit,
Trash2,
Key,
CheckCircle2,
XCircle,
UserPlus,
Settings
} from "lucide-react"
import { cn } from "@/lib/utils"
import type { User, UserRole } from "@/lib/types"
const roleLabels: Record<UserRole, string> = {
admin_sistema: "Administrador",
director_rrhh: "Director RR.HH.",
rrhh_regional: "RR.HH. Regional",
rrhh_local: "RR.HH. Local",
reclutador: "Reclutador",
administracion: "Administración",
country_lead: "Country Lead",
solicitante: "Solicitante"
}
const roleColors: Record<UserRole, string> = {
admin_sistema: "bg-purple-500 text-white",
director_rrhh: "bg-primary text-primary-foreground",
rrhh_regional: "bg-blue-500 text-white",
rrhh_local: "bg-blue-400 text-white",
reclutador: "bg-accent text-accent-foreground",
administracion: "bg-secondary text-secondary-foreground",
country_lead: "bg-orange-500 text-white",
solicitante: "bg-muted text-muted-foreground"
}
export function UsersManagementPage() {
const [users, setUsers] = useState(mockUsers)
const [searchQuery, setSearchQuery] = useState("")
const [filterRole, setFilterRole] = useState<string>("all")
const [filterDepartment, setFilterDepartment] = useState<string>("all")
const [filterStatus, setFilterStatus] = useState<string>("all")
const [isNewUserOpen, setIsNewUserOpen] = useState(false)
const [selectedUser, setSelectedUser] = useState<User | null>(null)
const [isEditOpen, setIsEditOpen] = useState(false)
const filteredUsers = users.filter(user => {
const matchesSearch = user.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
user.email.toLowerCase().includes(searchQuery.toLowerCase())
const matchesRole = filterRole === "all" || user.role === filterRole
const matchesDepartment = filterDepartment === "all" || user.departmentId === filterDepartment
const matchesStatus = filterStatus === "all" ||
(filterStatus === "active" && user.active) ||
(filterStatus === "inactive" && !user.active)
return matchesSearch && matchesRole && matchesDepartment && matchesStatus
})
const userStats = {
total: users.length,
active: users.filter(u => u.active).length,
inactive: users.filter(u => !u.active).length,
byRole: Object.keys(roleLabels).reduce((acc, role) => {
acc[role] = users.filter(u => u.role === role).length
return acc
}, {} as Record<string, number>)
}
const handleToggleStatus = (userId: string) => {
u.id === userId ? { ...u, active: !u.active } : u
}
const openEditDialog = (user: User) => {
setSelectedUser(user)
setIsEditOpen(true)
}
return (
<div className="space-y-6">
{/* Header */}
<div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
<div>
<h1 className="text-3xl font-bold tracking-tight">Gestion de Usuarios</h1>
<p className="text-muted-foreground">
Administra los usuarios del sistema y sus permisos
</p>
</div>
<Dialog open={isNewUserOpen} onOpenChange={setIsNewUserOpen}>
<DialogTrigger asChild>
<Button className="bg-accent hover:bg-accent/90 text-accent-foreground">
<UserPlus className="mr-2 h-4 w-4" />
Nuevo Usuario
</Button>
</DialogTrigger>
<DialogContent className="max-w-lg">
<DialogHeader>
<DialogTitle>Crear Nuevo Usuario</DialogTitle>
<DialogDescription>
Ingresa la informacion del nuevo usuario
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="name">Nombre Completo</Label>
<Input id="name" placeholder="Juan Perez" />
</div>
<div className="space-y-2">
<Label htmlFor="email">Email Corporativo</Label>
<Input id="email" type="email" placeholder="juan@gomezlee.com" />
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="role">Rol</Label>
<Select>
<SelectTrigger>
<SelectValue placeholder="Seleccionar rol" />
</SelectTrigger>
<SelectContent>
{Object.entries(roleLabels).map(([role, label]) => (
<SelectItem key={role} value={role}>{label}</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="department">Departamento</Label>
<Select>
<SelectTrigger>
<SelectValue placeholder="Seleccionar departamento" />
</SelectTrigger>
<SelectContent>
{mockDepartments.map(dept => (
<SelectItem key={dept.id} value={dept.id}>{dept.name}</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="position">Cargo</Label>
<Input id="position" placeholder="Ej: Gerente de Operaciones" />
</div>
<div className="flex items-center justify-between p-4 border rounded-lg">
<div>
<Label>Enviar Invitacion por Email</Label>
<p className="text-sm text-muted-foreground">
El usuario recibira un email para configurar su contrasena
</p>
</div>
<Switch defaultChecked />
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setIsNewUserOpen(false)}>
Cancelar
</Button>
<Button className="bg-accent hover:bg-accent/90" onClick={() => setIsNewUserOpen(false)}>
Crear Usuario
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
{/* Stats */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<Card>
<CardContent className="p-4">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-lg bg-primary/10 flex items-center justify-center">
<Users className="h-5 w-5 text-primary" />
</div>
<div>
<div className="text-2xl font-bold">{userStats.total}</div>
<div className="text-sm text-muted-foreground">Total Usuarios</div>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-lg bg-accent/10 flex items-center justify-center">
<CheckCircle2 className="h-5 w-5 text-accent" />
</div>
<div>
<div className="text-2xl font-bold">{userStats.active}</div>
<div className="text-sm text-muted-foreground">Activos</div>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-lg bg-destructive/10 flex items-center justify-center">
<XCircle className="h-5 w-5 text-destructive" />
</div>
<div>
<div className="text-2xl font-bold">{userStats.inactive}</div>
<div className="text-sm text-muted-foreground">Inactivos</div>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-lg bg-purple-500/10 flex items-center justify-center">
<Shield className="h-5 w-5 text-purple-500" />
</div>
<div>
<div className="text-2xl font-bold">{userStats.byRole["admin_sistema"] || 0}</div>
<div className="text-sm text-muted-foreground">Administradores</div>
</div>
</div>
</CardContent>
</Card>
</div>
{/* Filters */}
<Card>
<CardContent className="p-4">
<div className="flex flex-col gap-4 md:flex-row md:items-center">
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Input
placeholder="Buscar usuarios por nombre o email..."
className="pl-10"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
</div>
<Select value={filterRole} onValueChange={setFilterRole}>
<SelectTrigger className="w-full md:w-[180px]">
<SelectValue placeholder="Filtrar por rol" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">Todos los roles</SelectItem>
{Object.entries(roleLabels).map(([role, label]) => (
<SelectItem key={role} value={role}>{label}</SelectItem>
))}
</SelectContent>
</Select>
<Select value={filterDepartment} onValueChange={setFilterDepartment}>
<SelectTrigger className="w-full md:w-[180px]">
<SelectValue placeholder="Departamento" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">Todos</SelectItem>
{mockDepartments.map(dept => (
<SelectItem key={dept.id} value={dept.id}>{dept.name}</SelectItem>
))}
</SelectContent>
</Select>
<Select value={filterStatus} onValueChange={setFilterStatus}>
<SelectTrigger className="w-full md:w-[140px]">
<SelectValue placeholder="Estado" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">Todos</SelectItem>
<SelectItem value="active">Activos</SelectItem>
<SelectItem value="inactive">Inactivos</SelectItem>
</SelectContent>
</Select>
</div>
</CardContent>
</Card>
{/* Users Table */}
<Card>
<CardContent className="p-0">
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr className="border-b bg-muted/50">
<th className="text-left p-4 font-medium">Usuario</th>
<th className="text-left p-4 font-medium">Rol</th>
<th className="text-left p-4 font-medium">Departamento</th>
<th className="text-left p-4 font-medium">Estado</th>
<th className="text-left p-4 font-medium">Ultimo Acceso</th>
<th className="text-right p-4 font-medium">Acciones</th>
</tr>
</thead>
<tbody>
{filteredUsers.map(user => {
const dept = mockDepartments.find(d => d.id === user.departmentId)
return (
<tr key={user.id} className="border-b hover:bg-muted/30">
<td className="p-4">
<div className="flex items-center gap-3">
<Avatar className="h-10 w-10">
<AvatarImage src={user.avatar} />
<AvatarFallback className="bg-primary/10 text-primary">
{user.name.split(" ").map(n => n[0]).join("")}
</AvatarFallback>
</Avatar>
<div>
<div className="font-medium">{user.name}</div>
<div className="text-sm text-muted-foreground">{user.email}</div>
</div>
</div>
</td>
<td className="p-4">
<Badge className={roleColors[user.role]}>
{roleLabels[user.role]}
</Badge>
</td>
<td className="p-4">
<span className="text-sm">{dept?.name || "-"}</span>
</td>
<td className="p-4">
<div className="flex items-center gap-2">
<div className={cn(
"w-2 h-2 rounded-full",
user.active ? "bg-accent" : "bg-destructive"
)} />
<span className="text-sm">
{user.active ? "Activo" : "Inactivo"}
</span>
</div>
</td>
<td className="p-4">
<span className="text-sm text-muted-foreground">
{user.lastLogin ? new Date(user.lastLogin).toLocaleDateString("es-MX") : "Nunca"}
</span>
</td>
<td className="p-4 text-right">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon">
<MoreVertical className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => openEditDialog(user)}>
<Edit className="mr-2 h-4 w-4" />
Editar
</DropdownMenuItem>
<DropdownMenuItem>
<Key className="mr-2 h-4 w-4" />
Restablecer Contrasena
</DropdownMenuItem>
<DropdownMenuItem>
<Shield className="mr-2 h-4 w-4" />
Gestionar Permisos
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleToggleStatus(user.id)}>
{user.active ? (
<>
<XCircle className="mr-2 h-4 w-4" />
Desactivar
</>
) : (
<>
<CheckCircle2 className="mr-2 h-4 w-4" />
Activar
</>
)}
</DropdownMenuItem>
<DropdownMenuItem className="text-destructive">
<Trash2 className="mr-2 h-4 w-4" />
Eliminar
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</td>
</tr>
)
})}
</tbody>
</table>
</div>
</CardContent>
</Card>
{/* Edit User Dialog */}
<Dialog open={isEditOpen} onOpenChange={setIsEditOpen}>
<DialogContent className="max-w-lg">
{selectedUser && (
<>
<DialogHeader>
<DialogTitle>Editar Usuario</DialogTitle>
<DialogDescription>
Modifica la informacion del usuario
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="flex items-center gap-4">
<Avatar className="h-16 w-16">
<AvatarImage src={selectedUser.avatar} />
<AvatarFallback className="bg-primary/10 text-primary text-xl">
{selectedUser.name.split(" ").map(n => n[0]).join("")}
</AvatarFallback>
</Avatar>
<Button variant="outline" size="sm">Cambiar Foto</Button>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label>Nombre</Label>
<Input defaultValue={selectedUser.name} />
</div>
<div className="space-y-2">
<Label>Email</Label>
<Input defaultValue={selectedUser.email} />
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label>Rol</Label>
<Select defaultValue={selectedUser.role}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{Object.entries(roleLabels).map(([role, label]) => (
<SelectItem key={role} value={role}>{label}</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label>Departamento</Label>
<Select defaultValue={selectedUser.departmentId}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{mockDepartments.map(dept => (
<SelectItem key={dept.id} value={dept.id}>{dept.name}</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
<div className="space-y-2">
<Label>Cargo</Label>
<Input defaultValue={selectedUser.position || ""} />
</div>
<div className="flex items-center justify-between p-4 border rounded-lg">
<div>
<Label>Usuario Activo</Label>
<p className="text-sm text-muted-foreground">
Los usuarios inactivos no pueden acceder al sistema
</p>
</div>
<Switch defaultChecked={selectedUser.active} />
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setIsEditOpen(false)}>
Cancelar
</Button>
<Button className="bg-accent hover:bg-accent/90" onClick={() => setIsEditOpen(false)}>
Guardar Cambios
</Button>
</DialogFooter>
</>
)}
</DialogContent>
</Dialog>
</div>
)
}