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