488 lines
20 KiB
TypeScript
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>
|
|
)
|
|
}
|