Admin Users: unify People and Machine Agents into single 'Usuários' tab with type filter; keep Team/Convites tabs

This commit is contained in:
codex-bot 2025-10-21 10:41:38 -03:00
parent e04888ff4d
commit 231310a9fe

View file

@ -255,6 +255,14 @@ export function AdminUsersManager({ initialUsers, initialInvites, roleOptions, d
const [machineSelection, setMachineSelection] = useState<Set<string>>(new Set()) const [machineSelection, setMachineSelection] = useState<Set<string>>(new Set())
const [isBulkDeletingMachines, setIsBulkDeletingMachines] = useState(false) const [isBulkDeletingMachines, setIsBulkDeletingMachines] = useState(false)
const [bulkDeleteMachinesOpen, setBulkDeleteMachinesOpen] = useState(false) const [bulkDeleteMachinesOpen, setBulkDeleteMachinesOpen] = useState(false)
// Unificado (pessoas + máquinas)
const [usersSearch, setUsersSearch] = useState("")
const [usersTypeFilter, setUsersTypeFilter] = useState<"all" | "people" | "machines">("all")
const [usersCompanyFilter, setUsersCompanyFilter] = useState<string>("all")
const [usersTenantFilter, setUsersTenantFilter] = useState<string>("all")
const [usersSelection, setUsersSelection] = useState<Set<string>>(new Set())
const [isBulkDeletingUsersCombined, setIsBulkDeletingUsersCombined] = useState(false)
const [bulkDeleteUsersCombinedOpen, setBulkDeleteUsersCombinedOpen] = useState(false)
const [createDialogOpen, setCreateDialogOpen] = useState(false) const [createDialogOpen, setCreateDialogOpen] = useState(false)
const [createForm, setCreateForm] = useState({ const [createForm, setCreateForm] = useState({
name: "", name: "",
@ -331,6 +339,34 @@ export function AdminUsersManager({ initialUsers, initialInvites, roleOptions, d
}) })
}, [machineUsers, machinePersonaFilter, machineSearch]) }, [machineUsers, machinePersonaFilter, machineSearch])
const combinedBaseUsers = useMemo(() => {
if (usersTypeFilter === "people") return peopleUsers
if (usersTypeFilter === "machines") return machineUsers
return [...peopleUsers, ...machineUsers]
}, [peopleUsers, machineUsers, usersTypeFilter])
const filteredCombinedUsers = useMemo(() => {
const term = usersSearch.trim().toLowerCase()
return combinedBaseUsers.filter((user) => {
if (usersCompanyFilter !== "all" && user.companyId !== usersCompanyFilter) return false
if (usersTenantFilter !== "all" && (user.tenantId ?? defaultTenantId) !== usersTenantFilter) return false
if (!term) return true
const persona = (user.machinePersona ?? "").toLowerCase()
const machineId = extractMachineId(user.email) ?? ""
const haystack = [
user.name ?? "",
user.email ?? "",
user.companyName ?? "",
formatRole(user.role),
persona,
machineId,
]
.join(" ")
.toLowerCase()
return haystack.includes(term)
})
}, [combinedBaseUsers, usersSearch, usersCompanyFilter, usersTenantFilter, defaultTenantId])
useEffect(() => { useEffect(() => {
void (async () => { void (async () => {
try { try {
@ -669,6 +705,10 @@ export function AdminUsersManager({ initialUsers, initialInvites, roleOptions, d
setInviteSelection(checked ? new Set(invites.map((i) => i.id)) : new Set()) setInviteSelection(checked ? new Set(invites.map((i) => i.id)) : new Set())
} }
function toggleUsersCombinedSelectAll(checked: boolean) {
setUsersSelection(checked ? new Set(filteredCombinedUsers.map((u) => u.id)) : new Set())
}
async function performBulkDeleteUsers(ids: string[]) { async function performBulkDeleteUsers(ids: string[]) {
if (ids.length === 0) return if (ids.length === 0) return
const tasks = ids.map(async (id) => { const tasks = ids.map(async (id) => {
@ -727,6 +767,22 @@ export function AdminUsersManager({ initialUsers, initialInvites, roleOptions, d
setInvites((prev) => prev.map((i) => (ids.includes(i.id) && i.status === "pending" ? { ...i, status: "revoked", revokedAt: new Date().toISOString() } : i))) setInvites((prev) => prev.map((i) => (ids.includes(i.id) && i.status === "pending" ? { ...i, status: "revoked", revokedAt: new Date().toISOString() } : i)))
} }
async function performBulkDeleteUsersCombined(ids: string[]) {
if (ids.length === 0) return
const machineIds: string[] = []
const humanIds: string[] = []
ids.forEach((id) => {
const u = users.find((x) => x.id === id)
if (!u) return
if (u.role === "machine") machineIds.push(id)
else humanIds.push(id)
})
await Promise.allSettled([
performBulkDeleteMachines(machineIds),
performBulkDeleteUsers(humanIds),
])
}
async function handleSaveUser(event: React.FormEvent<HTMLFormElement>) { async function handleSaveUser(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault() event.preventDefault()
if (!editUser) return if (!editUser) return
@ -866,8 +922,7 @@ async function handleDeleteUser() {
<Tabs defaultValue="team" className="w-full"> <Tabs defaultValue="team" className="w-full">
<TabsList className="h-12 w-full justify-start rounded-xl bg-slate-100 p-1"> <TabsList className="h-12 w-full justify-start rounded-xl bg-slate-100 p-1">
<TabsTrigger value="team" className="rounded-lg">Equipe</TabsTrigger> <TabsTrigger value="team" className="rounded-lg">Equipe</TabsTrigger>
<TabsTrigger value="people" className="rounded-lg">Usuários</TabsTrigger> <TabsTrigger value="users" className="rounded-lg">Usuários</TabsTrigger>
<TabsTrigger value="machines" className="rounded-lg">Agentes de máquina</TabsTrigger>
<TabsTrigger value="invites" className="rounded-lg">Convites</TabsTrigger> <TabsTrigger value="invites" className="rounded-lg">Convites</TabsTrigger>
</TabsList> </TabsList>
@ -1089,11 +1144,11 @@ async function handleDeleteUser() {
</Card> </Card>
</TabsContent> </TabsContent>
<TabsContent value="people" className="mt-6 space-y-6"> <TabsContent value="users" className="mt-6 space-y-6">
<div className="flex flex-col gap-4 rounded-xl border border-slate-200 bg-white p-4 shadow-sm sm:flex-row sm:items-center sm:justify-between"> <div className="flex flex-col gap-4 rounded-xl border border-slate-200 bg-white p-4 shadow-sm sm:flex-row sm:items-center sm:justify-between">
<div className="space-y-1"> <div className="space-y-1">
<p className="text-sm font-semibold text-neutral-900">Usuários</p> <p className="text-sm font-semibold text-neutral-900">Usuários</p>
<p className="text-xs text-neutral-500">{filteredPeopleUsers.length} {filteredPeopleUsers.length === 1 ? "usuário" : "usuários"}</p> <p className="text-xs text-neutral-500">{filteredCombinedUsers.length} {filteredCombinedUsers.length === 1 ? "usuário" : "usuários"}</p>
</div> </div>
<Button onClick={handleOpenCreateUser} size="sm" className="gap-2 self-start sm:self-auto"> <Button onClick={handleOpenCreateUser} size="sm" className="gap-2 self-start sm:self-auto">
<IconUserPlus className="size-4" /> <IconUserPlus className="size-4" />
@ -1104,54 +1159,54 @@ async function handleDeleteUser() {
<div className="relative w-full md:max-w-sm"> <div className="relative w-full md:max-w-sm">
<IconSearch className="text-muted-foreground pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2" /> <IconSearch className="text-muted-foreground pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2" />
<Input <Input
value={peopleSearch} value={usersSearch}
onChange={(event) => setPeopleSearch(event.target.value)} onChange={(event) => setUsersSearch(event.target.value)}
placeholder="Buscar por nome, e-mail ou empresa..." placeholder="Buscar por nome, e-mail, empresa ou máquina..."
className="h-9 pl-9" className="h-9 pl-9"
/> />
</div> </div>
<div className="mt-3 flex flex-wrap items-center gap-3 md:mt-0"> <div className="mt-3 flex flex-wrap items-center gap-3 md:mt-0">
<Select value={peopleRoleFilter} onValueChange={(value) => setPeopleRoleFilter(value as "all" | "manager" | "collaborator")}> <Select value={usersTypeFilter} onValueChange={(v) => setUsersTypeFilter(v as typeof usersTypeFilter)}>
<SelectTrigger className="h-9 w-full sm:w-48"> <SelectTrigger className="h-9 w-full sm:w-40">
<SelectValue placeholder="Perfil" /> <SelectValue placeholder="Tipo" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="all">Todos os perfis</SelectItem> <SelectItem value="all">Todos</SelectItem>
<SelectItem value="manager">Gestores</SelectItem> <SelectItem value="people">Pessoas</SelectItem>
<SelectItem value="collaborator">Colaboradores</SelectItem> <SelectItem value="machines">Máquinas</SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
<Select value={peopleCompanyFilter} onValueChange={setPeopleCompanyFilter}> <Select value={usersCompanyFilter} onValueChange={setUsersCompanyFilter}>
<SelectTrigger className="h-9 w-full sm:w-56"> <SelectTrigger className="h-9 w-full sm:w-56">
<SelectValue placeholder="Empresa" /> <SelectValue placeholder="Empresa" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="all">Todas as empresas</SelectItem> <SelectItem value="all">Todas as empresas</SelectItem>
{companies.map((company) => ( {companies.map((company) => (
<SelectItem key={`people-company-${company.id}`} value={company.id}>{company.name}</SelectItem> <SelectItem key={`users-company-${company.id}`} value={company.id}>{company.name}</SelectItem>
))} ))}
</SelectContent> </SelectContent>
</Select> </Select>
<Select value={peopleTenantFilter} onValueChange={setPeopleTenantFilter}> <Select value={usersTenantFilter} onValueChange={setUsersTenantFilter}>
<SelectTrigger className="h-9 w-full sm:w-40"> <SelectTrigger className="h-9 w-full sm:w-40">
<SelectValue placeholder="Espaço" /> <SelectValue placeholder="Espaço" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="all">Todos os espaços</SelectItem> <SelectItem value="all">Todos os espaços</SelectItem>
{tenantOptions.map((t) => ( {tenantOptions.map((t) => (
<SelectItem key={`people-tenant-${t}`} value={t}>{formatTenantLabel(t, defaultTenantId)}</SelectItem> <SelectItem key={`users-tenant-${t}`} value={t}>{formatTenantLabel(t, defaultTenantId)}</SelectItem>
))} ))}
</SelectContent> </SelectContent>
</Select> </Select>
{(peopleSearch.trim().length > 0 || peopleRoleFilter !== "all" || peopleCompanyFilter !== "all" || peopleTenantFilter !== "all") ? ( {(usersSearch.trim().length > 0 || usersTypeFilter !== "all" || usersCompanyFilter !== "all" || usersTenantFilter !== "all") ? (
<Button <Button
variant="ghost" variant="ghost"
size="sm" size="sm"
onClick={() => { onClick={() => {
setPeopleSearch("") setUsersSearch("")
setPeopleRoleFilter("all") setUsersTypeFilter("all")
setPeopleCompanyFilter("all") setUsersCompanyFilter("all")
setPeopleTenantFilter("all") setUsersTenantFilter("all")
}} }}
> >
Limpar filtros Limpar filtros
@ -1161,8 +1216,8 @@ async function handleDeleteUser() {
variant="outline" variant="outline"
size="sm" size="sm"
className="gap-2 border-rose-200 text-rose-600 hover:bg-rose-50 hover:text-rose-700 disabled:text-rose-300 disabled:border-rose-100" className="gap-2 border-rose-200 text-rose-600 hover:bg-rose-50 hover:text-rose-700 disabled:text-rose-300 disabled:border-rose-100"
disabled={selectedPeopleUsers.length === 0 || isBulkDeletingPeople} disabled={usersSelection.size === 0 || isBulkDeletingUsersCombined}
onClick={() => setBulkDeletePeopleOpen(true)} onClick={() => setBulkDeleteUsersCombinedOpen(true)}
> >
<IconTrash className="size-4" /> Excluir selecionados <IconTrash className="size-4" /> Excluir selecionados
</Button> </Button>
@ -1171,7 +1226,7 @@ async function handleDeleteUser() {
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle>Usuários</CardTitle> <CardTitle>Usuários</CardTitle>
<CardDescription>Colaboradores e gestores vinculados às empresas.</CardDescription> <CardDescription>Pessoas e máquinas com acesso ao sistema.</CardDescription>
</CardHeader> </CardHeader>
<CardContent className="overflow-x-auto"> <CardContent className="overflow-x-auto">
<table className="min-w-full table-fixed divide-y divide-slate-200 text-sm"> <table className="min-w-full table-fixed divide-y divide-slate-200 text-sm">
@ -1180,14 +1235,15 @@ async function handleDeleteUser() {
<th className="w-10 py-3 pr-2 font-medium"> <th className="w-10 py-3 pr-2 font-medium">
<div className="flex items-center justify-center"> <div className="flex items-center justify-center">
<Checkbox <Checkbox
checked={allPeopleSelected || (somePeopleSelected && "indeterminate")} checked={usersSelection.size > 0 && usersSelection.size === filteredCombinedUsers.length || (usersSelection.size > 0 && usersSelection.size < filteredCombinedUsers.length && "indeterminate")}
onCheckedChange={(value) => togglePeopleSelectAll(!!value)} onCheckedChange={(value) => toggleUsersCombinedSelectAll(!!value)}
aria-label="Selecionar todos" aria-label="Selecionar todos"
/> />
</div> </div>
</th> </th>
<th className="py-3 pr-4 font-medium">Nome</th> <th className="py-3 pr-4 font-medium">Nome</th>
<th className="py-3 pr-4 font-medium">E-mail</th> <th className="py-3 pr-4 font-medium">E-mail</th>
<th className="py-3 pr-4 font-medium">Tipo</th>
<th className="py-3 pr-4 font-medium">Perfil</th> <th className="py-3 pr-4 font-medium">Perfil</th>
<th className="py-3 pr-4 font-medium">Empresa</th> <th className="py-3 pr-4 font-medium">Empresa</th>
<th className="py-3 pr-4 font-medium">Espaço</th> <th className="py-3 pr-4 font-medium">Espaço</th>
@ -1196,14 +1252,14 @@ async function handleDeleteUser() {
</tr> </tr>
</thead> </thead>
<tbody className="divide-y divide-slate-100"> <tbody className="divide-y divide-slate-100">
{filteredPeopleUsers.map((user) => ( {filteredCombinedUsers.map((user) => (
<tr key={user.id} className="hover:bg-slate-50"> <tr key={user.id} className="hover:bg-slate-50">
<td className="py-3 pr-2"> <td className="py-3 pr-2">
<div className="flex items-center justify-center"> <div className="flex items-center justify-center">
<Checkbox <Checkbox
checked={peopleSelection.has(user.id)} checked={usersSelection.has(user.id)}
onCheckedChange={(checked) => { onCheckedChange={(checked) => {
setPeopleSelection((prev) => { setUsersSelection((prev) => {
const next = new Set(prev) const next = new Set(prev)
if (checked) next.add(user.id) if (checked) next.add(user.id)
else next.delete(user.id) else next.delete(user.id)
@ -1214,9 +1270,22 @@ async function handleDeleteUser() {
/> />
</div> </div>
</td> </td>
<td className="py-3 pr-4 font-medium text-neutral-800">{user.name || "—"}</td> <td className="py-3 pr-4 font-medium text-neutral-800">{user.name || (user.role === "machine" ? "Máquina" : "—")}</td>
<td className="py-3 pr-4 text-neutral-600">{user.email}</td> <td className="py-3 pr-4 text-neutral-600">{user.email}</td>
<td className="py-3 pr-4 text-neutral-600">{formatRole(user.role)}</td> <td className="py-3 pr-4 text-neutral-600">{user.role === "machine" ? "Máquina" : "Pessoa"}</td>
<td className="py-3 pr-4 text-neutral-600">
{user.role === "machine" ? (
user.machinePersona ? (
<Badge variant={machinePersonaBadgeVariant(user.machinePersona)} className="rounded-full px-3 py-1 text-xs font-medium">
{formatMachinePersona(user.machinePersona)}
</Badge>
) : (
<span className="text-neutral-500">Sem persona</span>
)
) : (
formatRole(user.role)
)}
</td>
<td className="py-3 pr-4 text-neutral-600">{user.companyName ?? "—"}</td> <td className="py-3 pr-4 text-neutral-600">{user.companyName ?? "—"}</td>
<td className="py-3 pr-4 text-neutral-600">{formatTenantLabel(user.tenantId, defaultTenantId)}</td> <td className="py-3 pr-4 text-neutral-600">{formatTenantLabel(user.tenantId, defaultTenantId)}</td>
<td className="py-3 pr-4 text-neutral-500">{formatDate(user.createdAt)}</td> <td className="py-3 pr-4 text-neutral-500">{formatDate(user.createdAt)}</td>
@ -1233,6 +1302,11 @@ async function handleDeleteUser() {
> >
Editar Editar
</Button> </Button>
{user.role === "machine" ? (
<Button variant="ghost" size="sm" asChild>
<Link href={(extractMachineId(user.email) ? `/admin/machines/${extractMachineId(user.email)}` : "/admin/machines")}>Detalhes da máquina</Link>
</Button>
) : null}
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
@ -1249,10 +1323,10 @@ async function handleDeleteUser() {
</td> </td>
</tr> </tr>
))} ))}
{filteredPeopleUsers.length === 0 ? ( {filteredCombinedUsers.length === 0 ? (
<tr> <tr>
<td colSpan={8} className="py-6 text-center text-neutral-500"> <td colSpan={9} className="py-6 text-center text-neutral-500">
{peopleUsers.length === 0 {combinedBaseUsers.length === 0
? "Nenhum usuário cadastrado até o momento." ? "Nenhum usuário cadastrado até o momento."
: "Nenhum usuário corresponde aos filtros atuais."} : "Nenhum usuário corresponde aos filtros atuais."}
</td> </td>
@ -1264,154 +1338,6 @@ async function handleDeleteUser() {
</Card> </Card>
</TabsContent> </TabsContent>
<TabsContent value="machines" className="mt-6 space-y-6">
<div className="rounded-xl border border-slate-200 bg-white p-4 shadow-sm">
<div className="flex flex-col gap-1 sm:flex-row sm:items-center sm:justify-between">
<div>
<p className="text-sm font-semibold text-neutral-900">Agentes de máquina</p>
<p className="text-xs text-neutral-500">{filteredMachineUsers.length} {filteredMachineUsers.length === 1 ? "agente" : "agentes"} vinculados via desktop client.</p>
</div>
</div>
</div>
<div className="flex flex-col gap-3 rounded-xl border border-slate-200 bg-white p-4 shadow-sm sm:flex-row sm:items-center sm:justify-between">
<div className="relative w-full sm:max-w-xs">
<IconSearch className="text-muted-foreground pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2" />
<Input
value={machineSearch}
onChange={(event) => setMachineSearch(event.target.value)}
placeholder="Buscar por hostname, e-mail ou persona"
className="h-9 pl-9"
/>
</div>
<div className="flex flex-wrap items-center gap-3">
<Select value={machinePersonaFilter} onValueChange={(value) => setMachinePersonaFilter(value as typeof machinePersonaFilter)}>
<SelectTrigger className="h-9 w-full sm:w-56">
<SelectValue placeholder="Todas as personas" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">Todas as personas</SelectItem>
<SelectItem value="manager">Gestor</SelectItem>
<SelectItem value="collaborator">Colaborador</SelectItem>
<SelectItem value="unassigned">Sem persona</SelectItem>
</SelectContent>
</Select>
{(machineSearch.trim().length > 0 || machinePersonaFilter !== "all") ? (
<Button
variant="ghost"
size="sm"
onClick={() => {
setMachineSearch("")
setMachinePersonaFilter("all")
}}
>
Limpar filtros
</Button>
) : null}
<Button
variant="outline"
size="sm"
className="gap-2 border-rose-200 text-rose-600 hover:bg-rose-50 hover:text-rose-700 disabled:text-rose-300 disabled:border-rose-100"
disabled={selectedMachineUsers.length === 0 || isBulkDeletingMachines}
onClick={() => setBulkDeleteMachinesOpen(true)}
>
<IconTrash className="size-4" /> Remover selecionados
</Button>
</div>
</div>
<Card>
<CardHeader>
<CardTitle>Agentes de máquina</CardTitle>
<CardDescription>Contas provisionadas automaticamente via agente desktop. Ajustes de vínculo podem ser feitos em <Link href="/admin/machines" className="underline underline-offset-4">Admin Máquinas</Link>.</CardDescription>
</CardHeader>
<CardContent className="overflow-x-auto">
<table className="min-w-full table-fixed divide-y divide-slate-200 text-sm">
<thead>
<tr className="text-left text-xs uppercase tracking-wide text-neutral-500">
<th className="w-10 py-3 pr-2 font-medium">
<div className="flex items-center justify-center">
<Checkbox
checked={selectedMachineUsers.length > 0 && selectedMachineUsers.length === filteredMachineUsers.length || (selectedMachineUsers.length > 0 && selectedMachineUsers.length < filteredMachineUsers.length && "indeterminate")}
onCheckedChange={(value) => toggleMachinesSelectAll(!!value)}
aria-label="Selecionar todos"
/>
</div>
</th>
<th className="py-3 pr-4 font-medium">Identificação</th>
<th className="py-3 pr-4 font-medium">E-mail técnico</th>
<th className="py-3 pr-4 font-medium">Perfil</th>
<th className="py-3 pr-4 font-medium">Criado em</th>
<th className="py-3 font-medium">Ações</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-100">
{filteredMachineUsers.map((user) => {
const machineId = extractMachineId(user.email)
return (
<tr key={user.id} className="hover:bg-slate-50">
<td className="py-3 pr-2">
<div className="flex items-center justify-center">
<Checkbox
checked={machineSelection.has(user.id)}
onCheckedChange={(checked) => {
setMachineSelection((prev) => {
const next = new Set(prev)
if (checked) next.add(user.id)
else next.delete(user.id)
return next
})
}}
aria-label="Selecionar linha"
/>
</div>
</td>
<td className="py-3 pr-4 font-medium text-neutral-800">{user.name || "Máquina"}</td>
<td className="py-3 pr-4 text-neutral-600">{user.email}</td>
<td className="py-3 pr-4 text-neutral-600">
{user.machinePersona ? (
<Badge variant={machinePersonaBadgeVariant(user.machinePersona)} className="rounded-full px-3 py-1 text-xs font-medium">
{formatMachinePersona(user.machinePersona)}
</Badge>
) : (
<span className="text-neutral-500">Sem persona</span>
)}
</td>
<td className="py-3 pr-4 text-neutral-500">{formatDate(user.createdAt)}</td>
<td className="py-3">
<div className="flex flex-wrap gap-2">
<Button variant="outline" size="sm" onClick={() => setEditUserId(user.id)}>
Editar
</Button>
<Button variant="ghost" size="sm" asChild>
<Link href={machineId ? `/admin/machines/${machineId}` : "/admin/machines"}>Detalhes da máquina</Link>
</Button>
<Button
variant="outline"
size="sm"
className="border-rose-200 text-rose-600 hover:bg-rose-50 hover:text-rose-700"
onClick={() => setDeleteUserId(user.id)}
>
Remover acesso
</Button>
</div>
</td>
</tr>
)
})}
{filteredMachineUsers.length === 0 ? (
<tr>
<td colSpan={6} className="py-6 text-center text-neutral-500">
{machineUsers.length === 0
? "Nenhuma máquina provisionada ainda."
: "Nenhum agente encontrado para a busca atual."}
</td>
</tr>
) : null}
</tbody>
</table>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="invites" className="mt-6 space-y-6"> <TabsContent value="invites" className="mt-6 space-y-6">
<Card> <Card>
<CardHeader> <CardHeader>
@ -1723,6 +1649,54 @@ async function handleDeleteUser() {
</DialogContent> </DialogContent>
</Dialog> </Dialog>
<Dialog open={bulkDeleteUsersCombinedOpen} onOpenChange={setBulkDeleteUsersCombinedOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>Remover usuários selecionados</DialogTitle>
<DialogDescription>Pessoas perderão o acesso e máquinas serão desconectadas.</DialogDescription>
</DialogHeader>
<div className="max-h-64 space-y-2 overflow-auto">
{Array.from(usersSelection).slice(0, 5).map((id) => {
const u = users.find((x) => x.id === id)
if (!u) return null
return (
<div key={`users-del-${id}`} className="rounded-md bg-slate-100 px-3 py-2 text-sm">
{(u.name || u.email)} <span className="text-neutral-500"> {u.email}</span>
</div>
)
})}
{usersSelection.size > 5 ? (
<div className="px-3 text-xs text-neutral-500">+ {usersSelection.size - 5} outros</div>
) : null}
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setBulkDeleteUsersCombinedOpen(false)} disabled={isBulkDeletingUsersCombined}>
Cancelar
</Button>
<Button
variant="destructive"
disabled={isBulkDeletingUsersCombined}
onClick={async () => {
setIsBulkDeletingUsersCombined(true)
try {
await performBulkDeleteUsersCombined(Array.from(usersSelection))
setUsersSelection(new Set())
setBulkDeleteUsersCombinedOpen(false)
toast.success("Remoção concluída")
} catch (error) {
const message = error instanceof Error ? error.message : "Falha ao excluir selecionados"
toast.error(message)
} finally {
setIsBulkDeletingUsersCombined(false)
}
}}
>
Excluir selecionados
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<Sheet open={Boolean(editUser)} onOpenChange={(open) => (!open ? setEditUserId(null) : null)}> <Sheet open={Boolean(editUser)} onOpenChange={(open) => (!open ? setEditUserId(null) : null)}>
<SheetContent side="right" className="space-y-6 overflow-y-auto px-6 pb-10 sm:max-w-2xl"> <SheetContent side="right" className="space-y-6 overflow-y-auto px-6 pb-10 sm:max-w-2xl">