Admin Users: unify People and Machine Agents into single 'Usuários' tab with type filter; keep Team/Convites tabs
This commit is contained in:
parent
e04888ff4d
commit
231310a9fe
1 changed files with 157 additions and 183 deletions
|
|
@ -255,6 +255,14 @@ export function AdminUsersManager({ initialUsers, initialInvites, roleOptions, d
|
|||
const [machineSelection, setMachineSelection] = useState<Set<string>>(new Set())
|
||||
const [isBulkDeletingMachines, setIsBulkDeletingMachines] = 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 [createForm, setCreateForm] = useState({
|
||||
name: "",
|
||||
|
|
@ -331,6 +339,34 @@ export function AdminUsersManager({ initialUsers, initialInvites, roleOptions, d
|
|||
})
|
||||
}, [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(() => {
|
||||
void (async () => {
|
||||
try {
|
||||
|
|
@ -669,6 +705,10 @@ export function AdminUsersManager({ initialUsers, initialInvites, roleOptions, d
|
|||
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[]) {
|
||||
if (ids.length === 0) return
|
||||
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)))
|
||||
}
|
||||
|
||||
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>) {
|
||||
event.preventDefault()
|
||||
if (!editUser) return
|
||||
|
|
@ -866,8 +922,7 @@ async function handleDeleteUser() {
|
|||
<Tabs defaultValue="team" className="w-full">
|
||||
<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="people" className="rounded-lg">Usuários</TabsTrigger>
|
||||
<TabsTrigger value="machines" className="rounded-lg">Agentes de máquina</TabsTrigger>
|
||||
<TabsTrigger value="users" className="rounded-lg">Usuários</TabsTrigger>
|
||||
<TabsTrigger value="invites" className="rounded-lg">Convites</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
|
|
@ -1089,11 +1144,11 @@ async function handleDeleteUser() {
|
|||
</Card>
|
||||
</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="space-y-1">
|
||||
<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>
|
||||
<Button onClick={handleOpenCreateUser} size="sm" className="gap-2 self-start sm:self-auto">
|
||||
<IconUserPlus className="size-4" />
|
||||
|
|
@ -1104,54 +1159,54 @@ async function handleDeleteUser() {
|
|||
<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" />
|
||||
<Input
|
||||
value={peopleSearch}
|
||||
onChange={(event) => setPeopleSearch(event.target.value)}
|
||||
placeholder="Buscar por nome, e-mail ou empresa..."
|
||||
value={usersSearch}
|
||||
onChange={(event) => setUsersSearch(event.target.value)}
|
||||
placeholder="Buscar por nome, e-mail, empresa ou máquina..."
|
||||
className="h-9 pl-9"
|
||||
/>
|
||||
</div>
|
||||
<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")}>
|
||||
<SelectTrigger className="h-9 w-full sm:w-48">
|
||||
<SelectValue placeholder="Perfil" />
|
||||
<Select value={usersTypeFilter} onValueChange={(v) => setUsersTypeFilter(v as typeof usersTypeFilter)}>
|
||||
<SelectTrigger className="h-9 w-full sm:w-40">
|
||||
<SelectValue placeholder="Tipo" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">Todos os perfis</SelectItem>
|
||||
<SelectItem value="manager">Gestores</SelectItem>
|
||||
<SelectItem value="collaborator">Colaboradores</SelectItem>
|
||||
<SelectItem value="all">Todos</SelectItem>
|
||||
<SelectItem value="people">Pessoas</SelectItem>
|
||||
<SelectItem value="machines">Máquinas</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Select value={peopleCompanyFilter} onValueChange={setPeopleCompanyFilter}>
|
||||
<Select value={usersCompanyFilter} onValueChange={setUsersCompanyFilter}>
|
||||
<SelectTrigger className="h-9 w-full sm:w-56">
|
||||
<SelectValue placeholder="Empresa" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">Todas as empresas</SelectItem>
|
||||
{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>
|
||||
</Select>
|
||||
<Select value={peopleTenantFilter} onValueChange={setPeopleTenantFilter}>
|
||||
<Select value={usersTenantFilter} onValueChange={setUsersTenantFilter}>
|
||||
<SelectTrigger className="h-9 w-full sm:w-40">
|
||||
<SelectValue placeholder="Espaço" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">Todos os espaços</SelectItem>
|
||||
{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>
|
||||
</Select>
|
||||
{(peopleSearch.trim().length > 0 || peopleRoleFilter !== "all" || peopleCompanyFilter !== "all" || peopleTenantFilter !== "all") ? (
|
||||
{(usersSearch.trim().length > 0 || usersTypeFilter !== "all" || usersCompanyFilter !== "all" || usersTenantFilter !== "all") ? (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setPeopleSearch("")
|
||||
setPeopleRoleFilter("all")
|
||||
setPeopleCompanyFilter("all")
|
||||
setPeopleTenantFilter("all")
|
||||
setUsersSearch("")
|
||||
setUsersTypeFilter("all")
|
||||
setUsersCompanyFilter("all")
|
||||
setUsersTenantFilter("all")
|
||||
}}
|
||||
>
|
||||
Limpar filtros
|
||||
|
|
@ -1161,8 +1216,8 @@ async function handleDeleteUser() {
|
|||
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={selectedPeopleUsers.length === 0 || isBulkDeletingPeople}
|
||||
onClick={() => setBulkDeletePeopleOpen(true)}
|
||||
disabled={usersSelection.size === 0 || isBulkDeletingUsersCombined}
|
||||
onClick={() => setBulkDeleteUsersCombinedOpen(true)}
|
||||
>
|
||||
<IconTrash className="size-4" /> Excluir selecionados
|
||||
</Button>
|
||||
|
|
@ -1171,7 +1226,7 @@ async function handleDeleteUser() {
|
|||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Usuários</CardTitle>
|
||||
<CardDescription>Colaboradores e gestores vinculados às empresas.</CardDescription>
|
||||
<CardDescription>Pessoas e máquinas com acesso ao sistema.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="overflow-x-auto">
|
||||
<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">
|
||||
<div className="flex items-center justify-center">
|
||||
<Checkbox
|
||||
checked={allPeopleSelected || (somePeopleSelected && "indeterminate")}
|
||||
onCheckedChange={(value) => togglePeopleSelectAll(!!value)}
|
||||
checked={usersSelection.size > 0 && usersSelection.size === filteredCombinedUsers.length || (usersSelection.size > 0 && usersSelection.size < filteredCombinedUsers.length && "indeterminate")}
|
||||
onCheckedChange={(value) => toggleUsersCombinedSelectAll(!!value)}
|
||||
aria-label="Selecionar todos"
|
||||
/>
|
||||
</div>
|
||||
</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">Tipo</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">Espaço</th>
|
||||
|
|
@ -1196,14 +1252,14 @@ async function handleDeleteUser() {
|
|||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-100">
|
||||
{filteredPeopleUsers.map((user) => (
|
||||
{filteredCombinedUsers.map((user) => (
|
||||
<tr key={user.id} className="hover:bg-slate-50">
|
||||
<td className="py-3 pr-2">
|
||||
<div className="flex items-center justify-center">
|
||||
<Checkbox
|
||||
checked={peopleSelection.has(user.id)}
|
||||
checked={usersSelection.has(user.id)}
|
||||
onCheckedChange={(checked) => {
|
||||
setPeopleSelection((prev) => {
|
||||
setUsersSelection((prev) => {
|
||||
const next = new Set(prev)
|
||||
if (checked) next.add(user.id)
|
||||
else next.delete(user.id)
|
||||
|
|
@ -1214,9 +1270,22 @@ async function handleDeleteUser() {
|
|||
/>
|
||||
</div>
|
||||
</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">{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">{formatTenantLabel(user.tenantId, defaultTenantId)}</td>
|
||||
<td className="py-3 pr-4 text-neutral-500">{formatDate(user.createdAt)}</td>
|
||||
|
|
@ -1233,6 +1302,11 @@ async function handleDeleteUser() {
|
|||
>
|
||||
Editar
|
||||
</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
|
||||
variant="outline"
|
||||
size="sm"
|
||||
|
|
@ -1249,10 +1323,10 @@ async function handleDeleteUser() {
|
|||
</td>
|
||||
</tr>
|
||||
))}
|
||||
{filteredPeopleUsers.length === 0 ? (
|
||||
{filteredCombinedUsers.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={8} className="py-6 text-center text-neutral-500">
|
||||
{peopleUsers.length === 0
|
||||
<td colSpan={9} className="py-6 text-center text-neutral-500">
|
||||
{combinedBaseUsers.length === 0
|
||||
? "Nenhum usuário cadastrado até o momento."
|
||||
: "Nenhum usuário corresponde aos filtros atuais."}
|
||||
</td>
|
||||
|
|
@ -1264,154 +1338,6 @@ async function handleDeleteUser() {
|
|||
</Card>
|
||||
</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">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
|
|
@ -1723,6 +1649,54 @@ async function handleDeleteUser() {
|
|||
</DialogContent>
|
||||
</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)}>
|
||||
<SheetContent side="right" className="space-y-6 overflow-y-auto px-6 pb-10 sm:max-w-2xl">
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue