From 231310a9fe55ad4cbeeddf10c17a3e512db12291 Mon Sep 17 00:00:00 2001 From: codex-bot Date: Tue, 21 Oct 2025 10:41:38 -0300 Subject: [PATCH] =?UTF-8?q?Admin=20Users:=20unify=20People=20and=20Machine?= =?UTF-8?q?=20Agents=20into=20single=20'Usu=C3=A1rios'=20tab=20with=20type?= =?UTF-8?q?=20filter;=20keep=20Team/Convites=20tabs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/admin/admin-users-manager.tsx | 340 +++++++++---------- 1 file changed, 157 insertions(+), 183 deletions(-) diff --git a/src/components/admin/admin-users-manager.tsx b/src/components/admin/admin-users-manager.tsx index e7a2665..7d1423f 100644 --- a/src/components/admin/admin-users-manager.tsx +++ b/src/components/admin/admin-users-manager.tsx @@ -255,6 +255,14 @@ export function AdminUsersManager({ initialUsers, initialInvites, roleOptions, d const [machineSelection, setMachineSelection] = useState>(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("all") + const [usersTenantFilter, setUsersTenantFilter] = useState("all") + const [usersSelection, setUsersSelection] = useState>(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) { event.preventDefault() if (!editUser) return @@ -866,8 +922,7 @@ async function handleDeleteUser() { Equipe - Usuários - Agentes de máquina + Usuários Convites @@ -1089,11 +1144,11 @@ async function handleDeleteUser() { - +

Usuários

-

{filteredPeopleUsers.length} {filteredPeopleUsers.length === 1 ? "usuário" : "usuários"}

+

{filteredCombinedUsers.length} {filteredCombinedUsers.length === 1 ? "usuário" : "usuários"}

@@ -1171,7 +1226,7 @@ async function handleDeleteUser() { Usuários - Colaboradores e gestores vinculados às empresas. + Pessoas e máquinas com acesso ao sistema. @@ -1180,14 +1235,15 @@ async function handleDeleteUser() { + @@ -1196,14 +1252,14 @@ async function handleDeleteUser() { - {filteredPeopleUsers.map((user) => ( + {filteredCombinedUsers.map((user) => ( - + - + + @@ -1233,6 +1302,11 @@ async function handleDeleteUser() { > Editar + {user.role === "machine" ? ( + + ) : null} ))} - {filteredPeopleUsers.length === 0 ? ( + {filteredCombinedUsers.length === 0 ? ( - @@ -1264,154 +1338,6 @@ async function handleDeleteUser() { - -
-
-
-

Agentes de máquina

-

{filteredMachineUsers.length} {filteredMachineUsers.length === 1 ? "agente" : "agentes"} vinculados via desktop client.

-
-
-
-
-
- - setMachineSearch(event.target.value)} - placeholder="Buscar por hostname, e-mail ou persona" - className="h-9 pl-9" - /> -
-
- - {(machineSearch.trim().length > 0 || machinePersonaFilter !== "all") ? ( - - ) : null} - -
-
- - - Agentes de máquina - Contas provisionadas automaticamente via agente desktop. Ajustes de vínculo podem ser feitos em Admin ▸ Máquinas. - - -
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" />
Nome E-mailTipo Perfil Empresa Espaço
{ - 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() { />
{user.name || "—"}{user.name || (user.role === "machine" ? "Máquina" : "—")} {user.email}{formatRole(user.role)}{user.role === "machine" ? "Máquina" : "Pessoa"} + {user.role === "machine" ? ( + user.machinePersona ? ( + + {formatMachinePersona(user.machinePersona)} + + ) : ( + Sem persona + ) + ) : ( + formatRole(user.role) + )} + {user.companyName ?? "—"} {formatTenantLabel(user.tenantId, defaultTenantId)} {formatDate(user.createdAt)}
- {peopleUsers.length === 0 + + {combinedBaseUsers.length === 0 ? "Nenhum usuário cadastrado até o momento." : "Nenhum usuário corresponde aos filtros atuais."}
- - - - - - - - - - - - {filteredMachineUsers.map((user) => { - const machineId = extractMachineId(user.email) - return ( - - - - - - - - - ) - })} - {filteredMachineUsers.length === 0 ? ( - - - - ) : null} - -
-
- 0 && selectedMachineUsers.length === filteredMachineUsers.length || (selectedMachineUsers.length > 0 && selectedMachineUsers.length < filteredMachineUsers.length && "indeterminate")} - onCheckedChange={(value) => toggleMachinesSelectAll(!!value)} - aria-label="Selecionar todos" - /> -
-
IdentificaçãoE-mail técnicoPerfilCriado emAções
-
- { - setMachineSelection((prev) => { - const next = new Set(prev) - if (checked) next.add(user.id) - else next.delete(user.id) - return next - }) - }} - aria-label="Selecionar linha" - /> -
-
{user.name || "Máquina"}{user.email} - {user.machinePersona ? ( - - {formatMachinePersona(user.machinePersona)} - - ) : ( - Sem persona - )} - {formatDate(user.createdAt)} -
- - - -
-
- {machineUsers.length === 0 - ? "Nenhuma máquina provisionada ainda." - : "Nenhum agente encontrado para a busca atual."} -
-
-
- - @@ -1723,6 +1649,54 @@ async function handleDeleteUser() { + + + + Remover usuários selecionados + Pessoas perderão o acesso e máquinas serão desconectadas. + +
+ {Array.from(usersSelection).slice(0, 5).map((id) => { + const u = users.find((x) => x.id === id) + if (!u) return null + return ( +
+ {(u.name || u.email)} — {u.email} +
+ ) + })} + {usersSelection.size > 5 ? ( +
+ {usersSelection.size - 5} outros
+ ) : null} +
+ + + + +
+
+ (!open ? setEditUserId(null) : null)}>