diff --git a/docs/OPERATIONS.md b/docs/OPERATIONS.md index aaac97e..e804d4e 100644 --- a/docs/OPERATIONS.md +++ b/docs/OPERATIONS.md @@ -142,3 +142,41 @@ Depois disso, o job “Deploy Convex functions” funciona em modo não interati Última atualização: sincronizado após o deploy bem‑sucedido do Convex e do Front (20/10/2025). +## 9) Admin ▸ Usuários e Máquinas — Unificação e UX + +Resumo das mudanças aplicadas no painel administrativo para simplificar “Usuários” e “Agentes de máquina” e melhorar o filtro em Máquinas: + +- Unificação de “Usuários” e “Agentes de máquina” + - Antes: abas separadas “Usuários” (pessoas) e “Agentes de máquina”. + - Agora: uma só aba “Usuários” com filtro de tipo (Todos | Pessoas | Máquinas). + - Onde: `src/components/admin/admin-users-manager.tsx:923`, aba `value="users"` em `:1147`. + - Motivo: evitar confusão entre “usuário” e “agente”; agentes são um tipo especial de usuário (role=machine). A unificação torna “Convites e Acessos” mais direta. + +- Máquinas ▸ Filtro por Empresa com busca e remoção do filtro de SO + - Adicionado dropdown de “Empresa” com busca (Popover + Input) e removido o filtro por Sistema Operacional. + - Onde: `src/components/admin/machines/admin-machines-overview.tsx:1038` e `:1084`. + - Motivo: fluxo real usa empresas com mais frequência; filtro por SO era menos útil agora. + +- Windows ▸ Rótulo do sistema sem duplicidade do “major” + - Exemplo: “Windows 11 Pro (26100)” em vez de “Windows 11 Pro 11 (26100)”. + - Onde: `src/components/admin/machines/admin-machines-overview.tsx` (função `formatOsVersionDisplay`). + - Motivo: legibilidade e padronização em chips/cartões. + +- Vínculos visuais entre máquinas e pessoas + - Cards de máquinas mostram “Usuário vinculado” quando disponível (assignment/metadata): `src/components/admin/machines/admin-machines-overview.tsx:3198`. + - Editor de usuário exibe “Máquinas vinculadas” (derivado de assign/metadata): `src/components/admin/admin-users-manager.tsx` (seção “Máquinas vinculadas” no sheet de edição). + - Observação: por ora é leitura; ajustes detalhados de vínculo permanecem em Admin ▸ Máquinas. + +### Identidade, e‑mail e histórico (reinstalação) + +- Identificador imutável: o histórico (tickets, eventos) referencia o `userId` (imutável). O e‑mail é um atributo mutável. +- Reinstalação do desktop para o mesmo colaborador: reutilize a mesma conta de usuário (mesmo `userId`); se o e‑mail mudou, atualize o e‑mail dessa conta no painel. O histórico permanece, pois o `userId` não muda. +- Novo e‑mail como nova conta: se criar um usuário novo (novo `userId`), será considerado um colaborador distinto e não herdará o histórico. +- Caso precise migrar histórico entre contas diferentes (merge), recomendamos endpoint/rotina de “fusão de contas” (remapear `userId` antigo → novo). Não é necessário para a troca de e‑mail da mesma conta. + +### Onde editar + +- Usuários (pessoas): editar nome, e‑mail, papel, tenant e empresa; redefinir senha pelo painel. Arquivo: `src/components/admin/admin-users-manager.tsx`. +- Agentes (máquinas): provisionamento automático; edição detalhada/vínculo principal em Admin ▸ Máquinas. Arquivo: `src/components/admin/machines/admin-machines-overview.tsx`. + +> Observação operacional: mantivemos o provisionamento de máquinas inalterado (token/e‑mail técnico), e o acesso web segue apenas para pessoas. A unificação é de UX/gestão. diff --git a/src/components/admin/admin-users-manager.tsx b/src/components/admin/admin-users-manager.tsx index 7d1423f..4dfeb84 100644 --- a/src/components/admin/admin-users-manager.tsx +++ b/src/components/admin/admin-users-manager.tsx @@ -22,6 +22,9 @@ import { } from "@/components/ui/select" import { Sheet, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetTitle } from "@/components/ui/sheet" import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" +import { useQuery } from "convex/react" +import { api } from "@/convex/_generated/api" +import { useAuth } from "@/lib/auth-client" import { ROLE_OPTIONS, type RoleOption } from "@/lib/authz" import { canReactivateInvite as canReactivateInvitePolicy } from "@/lib/invite-policies" @@ -164,6 +167,7 @@ function isRestrictedRole(role?: string | null) { } export function AdminUsersManager({ initialUsers, initialInvites, roleOptions, defaultTenantId, viewerRole }: Props) { + const { convexUserId } = useAuth() const [users, setUsers] = useState(initialUsers) const [invites, setInvites] = useState(initialInvites) const [companies, setCompanies] = useState([]) @@ -274,6 +278,12 @@ export function AdminUsersManager({ initialUsers, initialInvites, roleOptions, d const [isCreatingUser, setIsCreatingUser] = useState(false) const [createPassword, setCreatePassword] = useState(null) + // Máquinas (para listar vínculos por usuário) + const machinesList = useQuery( + convexUserId ? api.machines.listByTenant : "skip", + convexUserId ? { tenantId: defaultTenantId, includeMetadata: true } : ("skip" as const) + ) as Array<{ id: string; hostname?: string; assignedUserEmail?: string | null; metadata?: unknown }> | undefined + // Options of tenants present in dataset for filtering const tenantOptions = useMemo(() => { const set = new Set() @@ -400,6 +410,28 @@ export function AdminUsersManager({ initialUsers, initialInvites, roleOptions, d }) setPasswordPreview(null) }, [editUser, defaultTenantId]) + + const linkedMachinesForEditUser = useMemo(() => { + if (!editUser || !machinesList) return [] as Array<{ id: string; hostname?: string }> + const email = (editUser.email ?? "").toLowerCase() + const results: Array<{ id: string; hostname?: string }> = [] + machinesList.forEach((m) => { + const assigned = (m.assignedUserEmail ?? "").toLowerCase() + let collaboratorEmail = "" + if (m.metadata && typeof m.metadata === "object") { + const rec = m.metadata as Record + const c = rec["collaborator"] + if (c && typeof c === "object") { + const base = c as Record + if (typeof base.email === "string") collaboratorEmail = base.email.toLowerCase() + } + } + if (assigned === email || (collaboratorEmail && collaboratorEmail === email)) { + results.push({ id: m.id, hostname: m.hostname }) + } + }) + return results + }, [editUser, machinesList]) useEffect(() => { setCreateForm((prev) => ({ ...prev, @@ -1783,6 +1815,23 @@ async function handleDeleteUser() {

Essa seleção substitui o vínculo atual no portal do cliente.

+
+ + {linkedMachinesForEditUser.length > 0 ? ( +
    + {linkedMachinesForEditUser.map((m) => ( +
  • + {m.hostname || m.id} + +
  • + ))} +
+ ) : ( +

Nenhuma máquina vinculada a este usuário.

+ )} +
{isMachineEditing ? (
Os ajustes detalhados de agentes de máquina são feitos em Admin ▸ Máquinas. diff --git a/src/components/admin/machines/admin-machines-overview.tsx b/src/components/admin/machines/admin-machines-overview.tsx index 3a519e3..d0c3334 100644 --- a/src/components/admin/machines/admin-machines-overview.tsx +++ b/src/components/admin/machines/admin-machines-overview.tsx @@ -3200,6 +3200,14 @@ function MachineCard({ machine, companyName }: { machine: MachinesQueryItem; com {machine.hostname} {machine.authEmail ?? "—"} + {collaborator?.email ? ( +
+ + Usuário vinculado: {collaborator.name ? `${collaborator.name} · ` : ""}{collaborator.email} + + gerenciar usuários +
+ ) : null} {!isActive ? ( Desativada