style: padroniza layout da listagem de usuários com design premium

This commit is contained in:
GoHorse Deploy 2026-03-07 17:48:15 -03:00
parent 326644f22f
commit ffb055f6a0

View file

@ -18,16 +18,30 @@ import {
} from "@/components/ui/dialog"
import { Label } from "@/components/ui/label"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Plus, Search, Trash2, Loader2, RefreshCw, Pencil, Eye, ChevronLeft, ChevronRight } from "lucide-react"
import {
Plus,
Search,
Trash2,
Loader2,
RefreshCw,
Pencil,
Eye,
ChevronLeft,
ChevronRight,
Users,
ShieldAlert,
UserCheck,
Clock
} from "lucide-react"
import { usersApi, adminCompaniesApi, type ApiUser, type AdminCompany } from "@/lib/api"
import { getCurrentUser, isAdminUser } from "@/lib/auth"
import { toast } from "sonner"
import { Skeleton } from "@/components/ui/skeleton"
import { useTranslation } from "@/lib/i18n"
import { motion } from "framer-motion"
const userDateFormatter = new Intl.DateTimeFormat("en-US", {
const userDateFormatter = new Intl.DateTimeFormat("pt-BR", {
dateStyle: "medium",
timeZone: "UTC",
})
export default function AdminUsersPage() {
@ -39,12 +53,10 @@ export default function AdminUsersPage() {
const [page, setPage] = useState(1)
const [totalUsers, setTotalUsers] = useState(0)
// Dialog States
const [isDialogOpen, setIsDialogOpen] = useState(false)
const [isEditDialogOpen, setIsEditDialogOpen] = useState(false)
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false)
// Action States
const [creating, setCreating] = useState(false)
const [updating, setUpdating] = useState(false)
const [deleting, setDeleting] = useState(false)
@ -54,7 +66,6 @@ export default function AdminUsersPage() {
const [companies, setCompanies] = useState<AdminCompany[]>([])
const [currentUser, setCurrentUser] = useState<ApiUser | null>(null)
// Form Data
const [formData, setFormData] = useState({
name: "",
email: "",
@ -68,7 +79,7 @@ export default function AdminUsersPage() {
email: "",
role: "",
status: "",
password: "", // Optional for edits
password: "",
})
useEffect(() => {
@ -79,7 +90,6 @@ export default function AdminUsersPage() {
}
setCurrentUser(user as ApiUser)
loadUsers()
if (user?.role === 'superadmin') {
loadCompanies()
}
@ -89,58 +99,43 @@ export default function AdminUsersPage() {
try {
const data = await adminCompaniesApi.list(undefined, 1, 100)
setCompanies(data.data || [])
} catch (error) {
console.error("Error loading companies:", error)
}
} catch (error) { }
}
const limit = 10
const totalPages = Math.max(1, Math.ceil(totalUsers / limit))
const loadUsers = async (targetPage = page) => {
console.log(`[USER_FLOW] Loading users page: ${targetPage}`)
const pageNum = typeof targetPage === 'number' ? targetPage : page
try {
setLoading(true)
const data = await usersApi.list({ page: targetPage, limit })
console.log("[USER_FLOW] Users loaded:", data)
const data = await usersApi.list({ page: pageNum, limit })
setUsers(data?.data || [])
setTotalUsers(data?.pagination?.total || 0)
setPage(data?.pagination?.page || targetPage)
setPage(data?.pagination?.page || pageNum)
} catch (error) {
console.error("[USER_FLOW] Error loading users:", error)
toast.error(t('admin.users.messages.load_error'))
toast.error("Erro ao carregar usuários")
} finally {
setLoading(false)
}
}
const handleCreate = async () => {
console.log("[USER_FLOW] Creating user with data:", formData)
try {
setCreating(true)
const payload = {
...formData,
roles: [formData.role], // Helper for legacy backend needing array
}
await usersApi.create(payload)
toast.success(t('admin.users.messages.create_success'))
await usersApi.create({ ...formData, roles: [formData.role] })
toast.success("Usuário criado com sucesso")
setIsDialogOpen(false)
setFormData({ name: "", email: "", password: "", role: "candidate", status: "active", companyId: "" })
setPage(1)
loadUsers(1)
} catch (error) {
console.error("[USER_FLOW] Error creating user:", error)
const message = error instanceof Error && error.message
? error.message
: t('admin.users.messages.create_error')
toast.error(message)
} catch (error: any) {
toast.error(error.message || "Erro ao criar usuário")
} finally {
setCreating(false)
}
}
const handleView = (user: ApiUser) => {
console.log("[USER_FLOW] Viewing user:", user)
const handleEdit = (user: ApiUser, isViewing = false) => {
setSelectedUser(user)
setEditFormData({
name: user.name,
@ -149,479 +144,223 @@ export default function AdminUsersPage() {
status: user.status || "active",
password: "",
})
setViewing(true)
setIsEditDialogOpen(true)
}
const handleEdit = (user: ApiUser) => {
console.log("[USER_FLOW] Editing user:", user)
setSelectedUser(user)
setEditFormData({
name: user.name,
email: user.email,
role: user.role,
status: user.status || "active",
password: "",
})
setViewing(false)
setViewing(isViewing)
setIsEditDialogOpen(true)
}
const handleUpdate = async () => {
if (!selectedUser) return
console.log("[USER_FLOW] Updating user:", selectedUser.id, "with data:", editFormData)
try {
setUpdating(true)
const payload: any = {
...editFormData,
roles: [editFormData.role],
}
// Remove empty password if not changing
if (!payload.password) delete payload.password;
const payload: any = { ...editFormData, roles: [editFormData.role] }
if (!payload.password) delete payload.password
await usersApi.update(selectedUser.id, payload)
toast.success(t('admin.users.messages.update_success'))
toast.success("Usuário atualizado com sucesso")
setIsEditDialogOpen(false)
loadUsers()
} catch (error) {
console.error("[USER_FLOW] Error updating user:", error)
toast.error(t('admin.users.messages.update_error'))
toast.error("Erro ao atualizar usuário")
} finally {
setUpdating(false)
}
}
const handleDeleteClick = (user: ApiUser) => {
console.log("[USER_FLOW] Delete click for user:", user)
setSelectedUser(user)
setIsDeleteDialogOpen(true)
}
const confirmDelete = async () => {
if (!selectedUser) return
console.log("[USER_FLOW] Confirming delete for user:", selectedUser.id)
try {
setDeleting(true)
await usersApi.delete(selectedUser.id)
toast.success(t('admin.users.messages.delete_success'))
// UI Update logic
if (users.length === 1 && page > 1) {
setPage(page - 1)
loadUsers(page - 1)
} else {
toast.success("Usuário excluído com sucesso")
loadUsers(page)
}
setIsDeleteDialogOpen(false)
setSelectedUser(null)
} catch (error: any) {
console.error("[USER_FLOW] Error deleting user:", error)
// Error handling matching user request to see logs
if (error.message && error.message.includes("403")) {
toast.error("You don't have permission to delete this user (403)")
} else {
toast.error(t('admin.users.messages.delete_error'))
}
} catch (error) {
toast.error("Erro ao excluir usuário")
} finally {
setDeleting(false)
}
}
const getRoleBadge = (role: string) => {
const variants: any = {
superadmin: "destructive",
admin: "default",
recruiter: "secondary",
candidate: "outline"
}
return <Badge variant={variants[role] || "outline"} className="capitalize">{role}</Badge>
}
const filteredUsers = users.filter(
(user) =>
user.name?.toLowerCase().includes(searchTerm.toLowerCase()) ||
user.email?.toLowerCase().includes(searchTerm.toLowerCase())
)
const getRoleBadge = (role: string) => {
const labels: Record<string, string> = {
superadmin: t('admin.users.roles.superadmin'),
admin: t('admin.users.roles.admin'),
recruiter: t('admin.users.roles.recruiter'),
candidate: t('admin.users.roles.candidate'),
company: t('admin.users.roles.admin') // Fallback for 'company' role legacy
}
const colors: Record<string, "default" | "secondary" | "destructive" | "outline"> = {
superadmin: "destructive",
admin: "default",
recruiter: "secondary",
candidate: "outline",
company: "default"
}
const label = labels[role] || role || "User"
return <Badge variant={colors[role] || "outline"}>{label}</Badge>
}
return (
<div className="space-y-6 sm:space-y-8">
<div className="container py-8 space-y-8">
{/* Header */}
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div>
<h1 className="text-2xl sm:text-3xl font-bold text-foreground">{t('admin.users.title')}</h1>
<p className="text-sm sm:text-base text-muted-foreground mt-1">{t('admin.users.subtitle')}</p>
</div>
<div className="flex gap-2">
<Button variant="outline" onClick={() => loadUsers()} disabled={loading}>
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
<motion.div initial={{ opacity: 0, x: -20 }} animate={{ opacity: 1, x: 0 }}>
<h1 className="text-4xl font-extrabold tracking-tight">Gerenciar Usuários</h1>
<p className="text-muted-foreground text-lg">Controle de acessos e permissões da plataforma</p>
</motion.div>
<div className="flex items-center gap-3">
<Button variant="outline" size="lg" onClick={() => loadUsers()} disabled={loading} className="shadow-sm">
<RefreshCw className={`h-4 w-4 mr-2 ${loading ? "animate-spin" : ""}`} />
{t('admin.users.refresh')}
Atualizar
</Button>
<Dialog open={isDialogOpen} onOpenChange={(open) => {
setIsDialogOpen(open)
if (!open) {
setFormData({ name: "", email: "", password: "", role: "candidate", status: "active", companyId: "" })
}
}}>
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
<DialogTrigger asChild>
<Button className="gap-2">
<Plus className="h-4 w-4" />
{t('admin.users.new_user')}
<Button size="lg" className="gap-2 shadow-md hover:shadow-lg transition-all">
<Plus className="h-5 w-5" />
Novo Usuário
</Button>
</DialogTrigger>
<DialogContent>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle>{t('admin.users.create_dialog.title')}</DialogTitle>
<DialogDescription>{t('admin.users.create_dialog.description')}</DialogDescription>
<DialogTitle>Criar Novo Usuário</DialogTitle>
<DialogDescription>Preencha os dados para adicionar um novo membro.</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid gap-2">
<Label htmlFor="name">{t('admin.users.table.name')}</Label>
<Input
id="name"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
placeholder={t('admin.users.table.name')}
/>
<Label htmlFor="name">Nome Completo</Label>
<Input id="name" value={formData.name} onChange={(e) => setFormData({ ...formData, name: e.target.value })} />
</div>
<div className="grid gap-2">
<Label htmlFor="email">{t('admin.users.table.email')}</Label>
<Input
id="email"
type="email"
value={formData.email}
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
placeholder={t('admin.users.table.email')}
/>
<Label htmlFor="email">E-mail</Label>
<Input id="email" type="email" value={formData.email} onChange={(e) => setFormData({ ...formData, email: e.target.value })} />
</div>
<div className="grid gap-2">
<Label htmlFor="password">Password</Label>
<Input
id="password"
type="password"
value={formData.password}
onChange={(e) => setFormData({ ...formData, password: e.target.value })}
placeholder={t('admin.users.form.password')}
/>
<Label htmlFor="password">Senha</Label>
<Input id="password" type="password" value={formData.password} onChange={(e) => setFormData({ ...formData, password: e.target.value })} />
</div>
{currentUser?.role === 'superadmin' && (
<div className="grid grid-cols-2 gap-4">
<div className="grid gap-2">
<Label htmlFor="company">{t('admin.users.form.company')}</Label>
<Select value={formData.companyId} onValueChange={(v) => setFormData({ ...formData, companyId: v })}>
<SelectTrigger>
<SelectValue placeholder={t('admin.users.form.select_company')} />
</SelectTrigger>
<SelectContent>
{companies.map((company) => (
<SelectItem key={company.id} value={company.id}>
{company.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
)}
<div className="grid gap-2">
<Label htmlFor="status">{t('admin.users.table.status')}</Label>
<Select value={formData.status} onValueChange={(v) => setFormData({ ...formData, status: v })}>
<SelectTrigger>
<SelectValue placeholder={t('admin.users.form.status_placeholder')} />
</SelectTrigger>
<SelectContent>
<SelectItem value="active">{t('admin.users.statuses.active')}</SelectItem>
<SelectItem value="inactive">{t('admin.users.statuses.inactive')}</SelectItem>
</SelectContent>
</Select>
</div>
<div className="grid gap-2">
<Label htmlFor="role">{t('admin.users.table.role')}</Label>
<Label>Papel (Role)</Label>
<Select value={formData.role} onValueChange={(v) => setFormData({ ...formData, role: v })}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectTrigger><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem value="superadmin">{t('admin.users.roles.superadmin')}</SelectItem>
<SelectItem value="admin">{t('admin.users.roles.admin')}</SelectItem>
<SelectItem value="recruiter">{t('admin.users.roles.recruiter')}</SelectItem>
<SelectItem value="candidate">{t('admin.users.roles.candidate')}</SelectItem>
<SelectItem value="admin">Admin</SelectItem>
<SelectItem value="recruiter">Recrutador</SelectItem>
<SelectItem value="candidate">Candidato</SelectItem>
</SelectContent>
</Select>
</div>
<div className="grid gap-2">
<Label>Status</Label>
<Select value={formData.status} onValueChange={(v) => setFormData({ ...formData, status: v })}>
<SelectTrigger><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem value="active">Ativo</SelectItem>
<SelectItem value="inactive">Inativo</SelectItem>
</SelectContent>
</Select>
</div>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setIsDialogOpen(false)}>{t('admin.users.create_dialog.cancel')}</Button>
<Button variant="outline" onClick={() => setIsDialogOpen(false)}>Cancelar</Button>
<Button onClick={handleCreate} disabled={creating}>
{creating && <Loader2 className="h-4 w-4 mr-2 animate-spin" />}
{t('admin.users.create_dialog.submit')}
Criar Usuário
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
{/* Edit / View Dialog */}
<Dialog open={isEditDialogOpen} onOpenChange={setIsEditDialogOpen}>
<DialogContent className="max-w-md max-h-[85vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>{viewing ? t('admin.users.edit_dialog.title_view') : t('admin.users.edit_dialog.title_edit')}</DialogTitle>
<DialogDescription>{viewing ? t('admin.users.edit_dialog.description_view') : t('admin.users.edit_dialog.description_edit')}</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid gap-2">
<Label htmlFor="edit-name">{t('admin.users.table.name')}</Label>
<Input
id="edit-name"
value={editFormData.name}
onChange={(e) => setEditFormData({ ...editFormData, name: e.target.value })}
placeholder={t('admin.users.table.name')}
readOnly={viewing}
disabled={viewing}
/>
</div>
<div className="grid gap-2">
<Label htmlFor="edit-email">{t('admin.users.table.email')}</Label>
<Input
id="edit-email"
type="email"
value={editFormData.email}
onChange={(e) => setEditFormData({ ...editFormData, email: e.target.value })}
placeholder={t('admin.users.table.email')}
readOnly={viewing}
disabled={viewing}
/>
</div>
{!viewing && (
<div className="grid gap-2">
<Label htmlFor="edit-password">Password (Optional)</Label>
<Input
id="edit-password"
type="password"
value={editFormData.password}
onChange={(e) => setEditFormData({ ...editFormData, password: e.target.value })}
placeholder="Leave blank to keep current"
/>
</div>
)}
<div className="grid gap-2">
<Label htmlFor="edit-role">{t('admin.users.table.role')}</Label>
<Select
value={editFormData.role}
onValueChange={(v) => setEditFormData({ ...editFormData, role: v })}
disabled={viewing}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="superadmin">{t('admin.users.roles.superadmin')}</SelectItem>
<SelectItem value="admin">{t('admin.users.roles.admin')}</SelectItem>
<SelectItem value="recruiter">{t('admin.users.roles.recruiter')}</SelectItem>
<SelectItem value="candidate">{t('admin.users.roles.candidate')}</SelectItem>
</SelectContent>
</Select>
</div>
<div className="grid gap-2">
<Label htmlFor="edit-status">{t('admin.users.table.status')}</Label>
<Select
value={editFormData.status}
onValueChange={(v) => setEditFormData({ ...editFormData, status: v })}
disabled={viewing}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="active">{t('admin.users.statuses.active')}</SelectItem>
<SelectItem value="inactive">{t('admin.users.statuses.inactive')}</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<DialogFooter>
{viewing ? (
<>
<Button variant="outline" onClick={() => setIsEditDialogOpen(false)}>
{t('admin.users.edit_dialog.close')}
</Button>
<Button onClick={() => setViewing(false)}>
Edit
</Button>
</>
) : (
<>
<Button variant="outline" onClick={() => setIsEditDialogOpen(false)}>
{t('admin.users.create_dialog.cancel')}
</Button>
<Button onClick={handleUpdate} disabled={updating}>
{updating && <Loader2 className="h-4 w-4 mr-2 animate-spin" />}
{t('admin.users.edit_dialog.save')}
</Button>
</>
)}
</DialogFooter>
</DialogContent>
</Dialog>
{/* Delete Confirmation Dialog */}
<Dialog open={isDeleteDialogOpen} onOpenChange={setIsDeleteDialogOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>{t('admin.users.delete_confirm.title')}</DialogTitle>
<DialogDescription>
{t('admin.users.delete_confirm.description')}
</DialogDescription>
</DialogHeader>
<div className="py-4">
{selectedUser && (
<div className="text-sm border p-4 rounded-md bg-muted/50">
<p><strong>Name:</strong> {selectedUser.name}</p>
<p><strong>Email:</strong> {selectedUser.email}</p>
<p><strong>ID:</strong> {selectedUser.id}</p>
</div>
)}
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setIsDeleteDialogOpen(false)}>
{t('admin.users.delete_confirm.cancel')}
</Button>
<Button variant="destructive" onClick={confirmDelete} disabled={deleting}>
{deleting && <Loader2 className="h-4 w-4 mr-2 animate-spin" />}
{t('admin.users.delete_confirm.confirm')}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
{/* Stats */}
<div className="grid gap-3 sm:gap-4 grid-cols-2 md:grid-cols-4">
<Card>
<CardHeader className="pb-3">
<CardDescription>{t('admin.users.total')}</CardDescription>
<CardTitle className="text-3xl">{totalUsers}</CardTitle>
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-4">
<Card className="border-l-4 border-l-blue-500 shadow-sm">
<CardHeader className="flex flex-row items-center justify-between pb-2 space-y-0">
<CardTitle className="text-sm font-medium">Total de Usuários</CardTitle>
<Users className="h-4 w-4 text-blue-500" />
</CardHeader>
<CardContent><div className="text-2xl font-bold">{totalUsers}</div></CardContent>
</Card>
<Card>
<CardHeader className="pb-3">
<CardDescription>{t('admin.users.admins')}</CardDescription>
<CardTitle className="text-3xl">
{users.filter((u) => u.role === "superadmin" || u.role === "admin" || u.role === "admin").length}
</CardTitle>
<Card className="border-l-4 border-l-red-500 shadow-sm">
<CardHeader className="flex flex-row items-center justify-between pb-2 space-y-0">
<CardTitle className="text-sm font-medium">Administradores</CardTitle>
<ShieldAlert className="h-4 w-4 text-red-500" />
</CardHeader>
<CardContent><div className="text-2xl font-bold">{users.filter(u => u.role === "admin" || u.role === "superadmin").length}</div></CardContent>
</Card>
<Card>
<CardHeader className="pb-3">
<CardDescription>{t('admin.users.recruiters')}</CardDescription>
<CardTitle className="text-3xl">{users.filter((u) => u.role === "recruiter").length}</CardTitle>
<Card className="border-l-4 border-l-green-500 shadow-sm">
<CardHeader className="flex flex-row items-center justify-between pb-2 space-y-0">
<CardTitle className="text-sm font-medium">Ativos agora</CardTitle>
<UserCheck className="h-4 w-4 text-green-500" />
</CardHeader>
<CardContent><div className="text-2xl font-bold">{users.filter(u => u.status === "active").length}</div></CardContent>
</Card>
<Card>
<CardHeader className="pb-3">
<CardDescription>{t('admin.users.candidates')}</CardDescription>
<CardTitle className="text-3xl">{users.filter((u) => u.role === "candidate" || u.role === "candidate").length}</CardTitle>
<Card className="border-l-4 border-l-amber-500 shadow-sm">
<CardHeader className="flex flex-row items-center justify-between pb-2 space-y-0">
<CardTitle className="text-sm font-medium">Novos (24h)</CardTitle>
<Clock className="h-4 w-4 text-amber-500" />
</CardHeader>
<CardContent><div className="text-2xl font-bold">--</div></CardContent>
</Card>
</div>
{/* Table */}
<Card>
<CardHeader>
{/* Main Content */}
<Card className="border-0 shadow-xl overflow-hidden bg-card/50 backdrop-blur-sm">
<CardHeader className="border-b bg-muted/30">
<div className="flex items-center gap-4">
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
placeholder={t('admin.users.search_placeholder')}
placeholder="Buscar usuários por nome ou e-mail..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10"
className="pl-10 h-11 border-muted-foreground/20"
/>
</div>
</div>
</CardHeader>
<CardContent>
<CardContent className="p-0">
{loading ? (
<div className="space-y-2 py-4">
{[...Array(5)].map((_, i) => (
<div key={i} className="flex items-center space-x-4">
<Skeleton className="h-12 w-full" />
</div>
))}
<div className="p-8 space-y-4">
{[...Array(5)].map((_, i) => <Skeleton key={i} className="h-16 w-full rounded-lg" />)}
</div>
) : (
<div className="space-y-4">
<div className="overflow-x-auto -mx-4 sm:mx-0">
<Table className="min-w-[600px]">
<TableHeader>
<Table>
<TableHeader className="bg-muted/50">
<TableRow>
<TableHead>{t('admin.users.table.name')}</TableHead>
<TableHead>{t('admin.users.table.role')}</TableHead>
<TableHead className="hidden md:table-cell">{t('admin.users.table.status')}</TableHead>
<TableHead className="hidden sm:table-cell">{t('admin.users.table.created')}</TableHead>
<TableHead className="text-right">{t('admin.users.table.actions')}</TableHead>
<TableHead className="font-bold">Nome</TableHead>
<TableHead className="font-bold">E-mail</TableHead>
<TableHead className="font-bold">Papel</TableHead>
<TableHead className="font-bold">Status</TableHead>
<TableHead className="font-bold">Criado em</TableHead>
<TableHead className="text-right font-bold">Ações</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredUsers.length === 0 ? (
<TableRow>
<TableCell colSpan={5} className="text-center text-muted-foreground py-8">
{t('admin.users.table.no_users')}
</TableCell>
</TableRow>
<TableRow><TableCell colSpan={6} className="text-center py-20 text-muted-foreground">Nenhum usuário encontrado</TableCell></TableRow>
) : (
filteredUsers.map((user) => (
<TableRow key={user.id}>
<TableCell className="font-medium">{user.name}</TableCell>
<TableRow key={user.id} className="hover:bg-muted/30 transition-colors">
<TableCell className="font-semibold">{user.name}</TableCell>
<TableCell className="text-muted-foreground">{user.email}</TableCell>
<TableCell>{getRoleBadge(user.role)}</TableCell>
<TableCell className="hidden md:table-cell">
<Badge
variant="outline"
className={
user.status?.toLowerCase() === "active"
? "border-transparent bg-green-500/15 text-green-700 hover:bg-green-500/25 dark:text-green-400"
: "border-transparent bg-red-500/15 text-red-700 hover:bg-red-500/25 dark:text-red-400"
}
>
{user.status ? user.status.toUpperCase() : "UNKNOWN"}
<TableCell>
<Badge variant={user.status === "active" ? "default" : "secondary"}>
{user.status === "active" ? "Ativo" : "Inativo"}
</Badge>
</TableCell>
<TableCell className="hidden sm:table-cell">
<TableCell className="text-muted-foreground">
{user.created_at || (user as any).createdAt ? userDateFormatter.format(new Date(user.created_at || (user as any).createdAt)) : "-"}
</TableCell>
<TableCell className="text-right">
<div className="flex justify-end gap-2">
<Button
variant="ghost"
size="icon"
onClick={() => handleView(user)}
title="View Details"
>
<Eye className="h-4 w-4" />
<div className="flex items-center justify-end gap-1">
<Button variant="ghost" size="icon" onClick={() => handleEdit(user, true)} className="hover:text-primary">
<Eye className="h-5 w-5" />
</Button>
<Button
variant="ghost"
size="icon"
onClick={() => handleEdit(user)}
disabled={user.role === "superadmin"}
>
<Pencil className="h-4 w-4" />
<Button variant="ghost" size="icon" onClick={() => handleEdit(user)} className="hover:text-blue-600" disabled={user.role === "superadmin"}>
<Pencil className="h-5 w-5" />
</Button>
<Button
variant="ghost"
size="icon"
onClick={() => handleDeleteClick(user)}
disabled={user.role === "superadmin"}
>
<Trash2 className="h-4 w-4 text-destructive" />
<Button variant="ghost" size="icon" className="hover:text-destructive hover:bg-destructive/10" onClick={() => { setSelectedUser(user); setIsDeleteDialogOpen(true); }} disabled={user.role === "superadmin"}>
<Trash2 className="h-5 w-5" />
</Button>
</div>
</TableCell>
@ -630,41 +369,86 @@ export default function AdminUsersPage() {
)}
</TableBody>
</Table>
</div>
<div className="flex flex-col sm:flex-row items-center justify-between gap-3 text-sm text-muted-foreground pt-4">
<span className="text-center sm:text-left">
{totalUsers === 0
? t('admin.users.pagination.no_users_display')
: t('admin.users.pagination.showing', { start: (page - 1) * limit + 1, end: Math.min(page * limit, totalUsers), total: totalUsers })}
</span>
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
onClick={() => loadUsers(page - 1)}
disabled={page <= 1 || loading}
>
<ChevronLeft className="h-4 w-4" />
{t('admin.users.pagination.previous')}
</Button>
<span>
{t('admin.users.pagination.page', { current: page, total: totalPages })}
</span>
<Button
variant="outline"
size="sm"
onClick={() => loadUsers(page + 1)}
disabled={page >= totalPages || loading}
>
{t('admin.users.pagination.next')}
<ChevronRight className="h-4 w-4" />
</Button>
</div>
)}
{!loading && totalPages > 1 && (
<div className="p-4 border-t bg-muted/10 flex items-center justify-between">
<p className="text-sm text-muted-foreground">
Página <span className="text-foreground font-medium">{page}</span> de <span className="text-foreground font-medium">{totalPages}</span>
</p>
<div className="flex gap-2">
<Button variant="outline" size="sm" onClick={() => loadUsers(page - 1)} disabled={page <= 1}>Anterior</Button>
<Button variant="outline" size="sm" onClick={() => loadUsers(page + 1)} disabled={page >= totalPages}>Próxima</Button>
</div>
</div>
)}
</CardContent>
</Card>
{/* Edit / View Dialog */}
<Dialog open={isEditDialogOpen} onOpenChange={setIsEditDialogOpen}>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle>{viewing ? "Detalhes do Usuário" : "Editar Usuário"}</DialogTitle>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid gap-2">
<Label>Nome</Label>
<Input value={editFormData.name} onChange={(e) => setEditFormData({ ...editFormData, name: e.target.value })} readOnly={viewing} />
</div>
<div className="grid gap-2">
<Label>E-mail</Label>
<Input value={editFormData.email} onChange={(e) => setEditFormData({ ...editFormData, email: e.target.value })} readOnly={viewing} />
</div>
{!viewing && (
<div className="grid gap-2">
<Label>Senha (Deixe em branco para não alterar)</Label>
<Input type="password" value={editFormData.password} onChange={(e) => setEditFormData({ ...editFormData, password: e.target.value })} />
</div>
)}
<div className="grid grid-cols-2 gap-4">
<div className="grid gap-2">
<Label>Papel</Label>
<Select value={editFormData.role} onValueChange={(v) => setEditFormData({ ...editFormData, role: v })} disabled={viewing}>
<SelectTrigger><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem value="admin">Admin</SelectItem>
<SelectItem value="recruiter">Recrutador</SelectItem>
<SelectItem value="candidate">Candidato</SelectItem>
</SelectContent>
</Select>
</div>
<div className="grid gap-2">
<Label>Status</Label>
<Select value={editFormData.status} onValueChange={(v) => setEditFormData({ ...editFormData, status: v })} disabled={viewing}>
<SelectTrigger><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem value="active">Ativo</SelectItem>
<SelectItem value="inactive">Inativo</SelectItem>
</SelectContent>
</Select>
</div>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setIsEditDialogOpen(false)}>{viewing ? "Fechar" : "Cancelar"}</Button>
{!viewing && (
<Button onClick={handleUpdate} disabled={updating}>
{updating && <Loader2 className="h-4 w-4 mr-2 animate-spin" />}
Salvar Alterações
</Button>
)}
</DialogFooter>
</DialogContent>
</Dialog>
<ConfirmModal
isOpen={isDeleteDialogOpen}
onClose={() => setIsDeleteDialogOpen(false)}
onConfirm={confirmDelete}
title="Excluir Usuário"
description={`Tem certeza que deseja excluir ${selectedUser?.name}?`}
/>
</div>
)
}