style: padroniza layout da listagem de usuários com design premium
This commit is contained in:
parent
326644f22f
commit
ffb055f6a0
1 changed files with 237 additions and 453 deletions
|
|
@ -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,522 +144,311 @@ 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")
|
||||||
|
loadUsers(page)
|
||||||
// UI Update logic
|
|
||||||
if (users.length === 1 && page > 1) {
|
|
||||||
setPage(page - 1)
|
|
||||||
loadUsers(page - 1)
|
|
||||||
} else {
|
|
||||||
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 })}>
|
<Select value={formData.role} onValueChange={(v) => setFormData({ ...formData, role: v })}>
|
||||||
<SelectTrigger>
|
<SelectTrigger><SelectValue /></SelectTrigger>
|
||||||
<SelectValue placeholder={t('admin.users.form.select_company')} />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{companies.map((company) => (
|
<SelectItem value="admin">Admin</SelectItem>
|
||||||
<SelectItem key={company.id} value={company.id}>
|
<SelectItem value="recruiter">Recrutador</SelectItem>
|
||||||
{company.name}
|
<SelectItem value="candidate">Candidato</SelectItem>
|
||||||
</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>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</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 })}>
|
|
||||||
<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>
|
||||||
</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 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>
|
</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]">
|
<TableRow>
|
||||||
<TableHeader>
|
<TableHead className="font-bold">Nome</TableHead>
|
||||||
<TableRow>
|
<TableHead className="font-bold">E-mail</TableHead>
|
||||||
<TableHead>{t('admin.users.table.name')}</TableHead>
|
<TableHead className="font-bold">Papel</TableHead>
|
||||||
<TableHead>{t('admin.users.table.role')}</TableHead>
|
<TableHead className="font-bold">Status</TableHead>
|
||||||
<TableHead className="hidden md:table-cell">{t('admin.users.table.status')}</TableHead>
|
<TableHead className="font-bold">Criado em</TableHead>
|
||||||
<TableHead className="hidden sm:table-cell">{t('admin.users.table.created')}</TableHead>
|
<TableHead className="text-right font-bold">Ações</TableHead>
|
||||||
<TableHead className="text-right">{t('admin.users.table.actions')}</TableHead>
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{filteredUsers.length === 0 ? (
|
||||||
|
<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} 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>
|
||||||
|
<Badge variant={user.status === "active" ? "default" : "secondary"}>
|
||||||
|
{user.status === "active" ? "Ativo" : "Inativo"}
|
||||||
|
</Badge>
|
||||||
|
</TableCell>
|
||||||
|
<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 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)} className="hover:text-blue-600" disabled={user.role === "superadmin"}>
|
||||||
|
<Pencil className="h-5 w-5" />
|
||||||
|
</Button>
|
||||||
|
<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>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
))
|
||||||
<TableBody>
|
)}
|
||||||
{filteredUsers.length === 0 ? (
|
</TableBody>
|
||||||
<TableRow>
|
</Table>
|
||||||
<TableCell colSpan={5} className="text-center text-muted-foreground py-8">
|
)}
|
||||||
{t('admin.users.table.no_users')}
|
|
||||||
</TableCell>
|
{!loading && totalPages > 1 && (
|
||||||
</TableRow>
|
<div className="p-4 border-t bg-muted/10 flex items-center justify-between">
|
||||||
) : (
|
<p className="text-sm text-muted-foreground">
|
||||||
filteredUsers.map((user) => (
|
Página <span className="text-foreground font-medium">{page}</span> de <span className="text-foreground font-medium">{totalPages}</span>
|
||||||
<TableRow key={user.id}>
|
</p>
|
||||||
<TableCell className="font-medium">{user.name}</TableCell>
|
<div className="flex gap-2">
|
||||||
<TableCell>{getRoleBadge(user.role)}</TableCell>
|
<Button variant="outline" size="sm" onClick={() => loadUsers(page - 1)} disabled={page <= 1}>Anterior</Button>
|
||||||
<TableCell className="hidden md:table-cell">
|
<Button variant="outline" size="sm" onClick={() => loadUsers(page + 1)} disabled={page >= totalPages}>Próxima</Button>
|
||||||
<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"}
|
|
||||||
</Badge>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="hidden sm:table-cell">
|
|
||||||
{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" />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
onClick={() => handleEdit(user)}
|
|
||||||
disabled={user.role === "superadmin"}
|
|
||||||
>
|
|
||||||
<Pencil className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
onClick={() => handleDeleteClick(user)}
|
|
||||||
disabled={user.role === "superadmin"}
|
|
||||||
>
|
|
||||||
<Trash2 className="h-4 w-4 text-destructive" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</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>
|
|
||||||
</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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue