Fix types: remove any; clean unused states; add machines summary in unified Users list; capitalize 'Gerenciar usuários'; correct Convex linkUser typing
This commit is contained in:
parent
89c8e0cdb3
commit
8b02b8a564
3 changed files with 58 additions and 153 deletions
|
|
@ -1089,9 +1089,9 @@ export const linkUser = mutation({
|
|||
.first()
|
||||
if (!user) throw new ConvexError("Usuário não encontrado")
|
||||
|
||||
const current = new Set((machine.linkedUserIds ?? []).map((id) => id.id ?? id))
|
||||
const current = new Set<Id<"users">>(machine.linkedUserIds ?? [])
|
||||
current.add(user._id)
|
||||
await ctx.db.patch(machine._id, { linkedUserIds: Array.from(current) as any, updatedAt: Date.now() })
|
||||
await ctx.db.patch(machine._id, { linkedUserIds: Array.from(current), updatedAt: Date.now() })
|
||||
return { ok: true }
|
||||
},
|
||||
})
|
||||
|
|
|
|||
|
|
@ -246,19 +246,7 @@ export function AdminUsersManager({ initialUsers, initialInvites, roleOptions, d
|
|||
const [teamSelection, setTeamSelection] = useState<Set<string>>(new Set())
|
||||
const [isBulkDeletingTeam, setIsBulkDeletingTeam] = useState(false)
|
||||
const [bulkDeleteTeamOpen, setBulkDeleteTeamOpen] = useState(false)
|
||||
// Usuários
|
||||
const [peopleSearch, setPeopleSearch] = useState("")
|
||||
const [peopleRoleFilter, setPeopleRoleFilter] = useState<"all" | "manager" | "collaborator">("all")
|
||||
const [peopleCompanyFilter, setPeopleCompanyFilter] = useState<string>("all")
|
||||
const [peopleTenantFilter, setPeopleTenantFilter] = useState<string>("all")
|
||||
const [peopleSelection, setPeopleSelection] = useState<Set<string>>(new Set())
|
||||
const [isBulkDeletingPeople, setIsBulkDeletingPeople] = useState(false)
|
||||
const [bulkDeletePeopleOpen, setBulkDeletePeopleOpen] = useState(false)
|
||||
const [machineSearch, setMachineSearch] = useState("")
|
||||
const [machinePersonaFilter, setMachinePersonaFilter] = useState<"all" | "manager" | "collaborator" | "unassigned">("all")
|
||||
const [machineSelection, setMachineSelection] = useState<Set<string>>(new Set())
|
||||
const [isBulkDeletingMachines, setIsBulkDeletingMachines] = useState(false)
|
||||
const [bulkDeleteMachinesOpen, setBulkDeleteMachinesOpen] = useState(false)
|
||||
// Removidos filtros antigos de Pessoas/Máquinas (agora unificado)
|
||||
// Unificado (pessoas + máquinas)
|
||||
const [usersSearch, setUsersSearch] = useState("")
|
||||
const [usersTypeFilter, setUsersTypeFilter] = useState<"all" | "people" | "machines">("all")
|
||||
|
|
@ -279,10 +267,45 @@ export function AdminUsersManager({ initialUsers, initialInvites, roleOptions, d
|
|||
const [createPassword, setCreatePassword] = useState<string | null>(null)
|
||||
|
||||
// Máquinas (para listar vínculos por usuário)
|
||||
type MachinesListItem = {
|
||||
id: string
|
||||
hostname?: string
|
||||
assignedUserEmail?: string | null
|
||||
metadata?: unknown
|
||||
linkedUsers?: Array<{ id: string; email: string; name: string }>
|
||||
}
|
||||
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
|
||||
) as MachinesListItem[] | undefined
|
||||
|
||||
const machinesByUserEmail = useMemo(() => {
|
||||
const map = new Map<string, Array<{ id: string; hostname?: string }>>()
|
||||
;(machinesList ?? []).forEach((m) => {
|
||||
const push = (email?: string | null) => {
|
||||
const e = (email ?? '').toLowerCase()
|
||||
if (!e) return
|
||||
const arr = map.get(e) ?? []
|
||||
arr.push({ id: m.id, hostname: m.hostname })
|
||||
map.set(e, arr)
|
||||
}
|
||||
push(m.assignedUserEmail)
|
||||
// metadata collaborator
|
||||
if (m.metadata && typeof m.metadata === 'object') {
|
||||
const rec = m.metadata as Record<string, unknown>
|
||||
const c = rec['collaborator']
|
||||
if (c && typeof c === 'object') {
|
||||
const base = c as Record<string, unknown>
|
||||
if (typeof base.email === 'string') push(base.email)
|
||||
}
|
||||
}
|
||||
// linked users
|
||||
if (Array.isArray(m.linkedUsers)) {
|
||||
m.linkedUsers.forEach((lu) => push(lu.email))
|
||||
}
|
||||
})
|
||||
return map
|
||||
}, [machinesList])
|
||||
|
||||
// Options of tenants present in dataset for filtering
|
||||
const tenantOptions = useMemo(() => {
|
||||
|
|
@ -311,43 +334,9 @@ export function AdminUsersManager({ initialUsers, initialInvites, roleOptions, d
|
|||
})
|
||||
}, [teamUsers, teamSearch, teamRoleFilter, teamCompanyFilter, teamTenantFilter, defaultTenantId])
|
||||
|
||||
const filteredPeopleUsers = useMemo(() => {
|
||||
const term = peopleSearch.trim().toLowerCase()
|
||||
return peopleUsers.filter((user) => {
|
||||
const role = coerceRole(user.role)
|
||||
if (peopleRoleFilter !== "all" && role !== peopleRoleFilter) return false
|
||||
if (peopleCompanyFilter !== "all" && user.companyId !== peopleCompanyFilter) return false
|
||||
if (peopleTenantFilter !== "all" && (user.tenantId ?? defaultTenantId) !== peopleTenantFilter) return false
|
||||
if (!term) return true
|
||||
const haystack = [
|
||||
user.name ?? "",
|
||||
user.email ?? "",
|
||||
user.companyName ?? "",
|
||||
formatRole(user.role),
|
||||
]
|
||||
.join(" ")
|
||||
.toLowerCase()
|
||||
return haystack.includes(term)
|
||||
})
|
||||
}, [peopleUsers, peopleSearch, peopleRoleFilter, peopleCompanyFilter, peopleTenantFilter, defaultTenantId])
|
||||
// Removido: lista específica de Pessoas (uso substituído pelo unificado)
|
||||
|
||||
const filteredMachineUsers = useMemo(() => {
|
||||
const term = machineSearch.trim().toLowerCase()
|
||||
return machineUsers.filter((user) => {
|
||||
const persona = (user.machinePersona ?? "unassigned").toLowerCase()
|
||||
if (machinePersonaFilter !== "all") {
|
||||
if (machinePersonaFilter === "unassigned" && persona !== "unassigned") return false
|
||||
if (machinePersonaFilter !== "unassigned" && persona !== machinePersonaFilter) return false
|
||||
}
|
||||
if (!term) return true
|
||||
return (
|
||||
(user.name ?? "").toLowerCase().includes(term) ||
|
||||
user.email.toLowerCase().includes(term) ||
|
||||
persona.includes(term) ||
|
||||
(extractMachineId(user.email) ?? "").toLowerCase().includes(term)
|
||||
)
|
||||
})
|
||||
}, [machineUsers, machinePersonaFilter, machineSearch])
|
||||
// Removido: filtro específico de agentes (uso substituído pelo unificado)
|
||||
|
||||
const combinedBaseUsers = useMemo(() => {
|
||||
if (usersTypeFilter === "people") return peopleUsers
|
||||
|
|
@ -426,8 +415,8 @@ export function AdminUsersManager({ initialUsers, initialInvites, roleOptions, d
|
|||
if (typeof base.email === "string") collaboratorEmail = base.email.toLowerCase()
|
||||
}
|
||||
}
|
||||
const linked = Array.isArray((m as any).linkedUsers)
|
||||
? ((m as any).linkedUsers as Array<{ email?: string }>).some((lu) => (lu.email ?? '').toLowerCase() === email)
|
||||
const linked = Array.isArray(m.linkedUsers)
|
||||
? m.linkedUsers.some((lu) => (lu.email ?? '').toLowerCase() === email)
|
||||
: false
|
||||
if (assigned === email || (collaboratorEmail && collaboratorEmail === email) || linked) {
|
||||
results.push({ id: m.id, hostname: m.hostname })
|
||||
|
|
@ -714,11 +703,9 @@ export function AdminUsersManager({ initialUsers, initialInvites, roleOptions, d
|
|||
const allTeamSelected = selectedTeamUsers.length > 0 && selectedTeamUsers.length === filteredTeamUsers.length
|
||||
const someTeamSelected = selectedTeamUsers.length > 0 && !allTeamSelected
|
||||
|
||||
const selectedPeopleUsers = useMemo(() => filteredPeopleUsers.filter((u) => peopleSelection.has(u.id)), [filteredPeopleUsers, peopleSelection])
|
||||
const allPeopleSelected = selectedPeopleUsers.length > 0 && selectedPeopleUsers.length === filteredPeopleUsers.length
|
||||
const somePeopleSelected = selectedPeopleUsers.length > 0 && !allPeopleSelected
|
||||
// Removido: seleção específica de Pessoas (uso substituído pelo unificado)
|
||||
|
||||
const selectedMachineUsers = useMemo(() => filteredMachineUsers.filter((u) => machineSelection.has(u.id)), [filteredMachineUsers, machineSelection])
|
||||
// Removido: seleção específica de Máquinas (uso substituído pelo unificado)
|
||||
|
||||
const [inviteSelection, setInviteSelection] = useState<Set<string>>(new Set())
|
||||
const selectedInvites = useMemo(() => invites.filter((i) => inviteSelection.has(i.id)), [invites, inviteSelection])
|
||||
|
|
@ -730,12 +717,7 @@ export function AdminUsersManager({ initialUsers, initialInvites, roleOptions, d
|
|||
function toggleTeamSelectAll(checked: boolean) {
|
||||
setTeamSelection(checked ? new Set(filteredTeamUsers.map((u) => u.id)) : new Set())
|
||||
}
|
||||
function togglePeopleSelectAll(checked: boolean) {
|
||||
setPeopleSelection(checked ? new Set(filteredPeopleUsers.map((u) => u.id)) : new Set())
|
||||
}
|
||||
function toggleMachinesSelectAll(checked: boolean) {
|
||||
setMachineSelection(checked ? new Set(filteredMachineUsers.map((u) => u.id)) : new Set())
|
||||
}
|
||||
// Removidos: toggles de seleção específicos (uso substituído pelo unificado)
|
||||
function toggleInvitesSelectAll(checked: boolean) {
|
||||
setInviteSelection(checked ? new Set(invites.map((i) => i.id)) : new Set())
|
||||
}
|
||||
|
|
@ -1063,6 +1045,7 @@ async function handleDeleteUser() {
|
|||
<th className="py-3 pr-4 font-medium">E-mail</th>
|
||||
<th className="py-3 pr-4 font-medium">Papel</th>
|
||||
<th className="py-3 pr-4 font-medium">Empresa</th>
|
||||
<th className="py-3 pr-4 font-medium">Máquinas</th>
|
||||
<th className="py-3 pr-4 font-medium">Espaço</th>
|
||||
<th className="py-3 pr-4 font-medium">Criado em</th>
|
||||
<th className="py-3 font-medium">Ações</th>
|
||||
|
|
@ -1091,6 +1074,14 @@ async function handleDeleteUser() {
|
|||
<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.companyName ?? "—"}</td>
|
||||
<td className="py-3 pr-4 text-neutral-600">
|
||||
{(() => {
|
||||
const list = machinesByUserEmail.get((user.email ?? '').toLowerCase()) ?? []
|
||||
return list.length > 0 ? (
|
||||
<span className="text-xs font-medium">{list.length} {list.length === 1 ? 'máquina' : 'máquinas'}</span>
|
||||
) : '—'
|
||||
})()}
|
||||
</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">
|
||||
|
|
@ -2012,93 +2003,7 @@ async function handleDeleteUser() {
|
|||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog open={bulkDeletePeopleOpen} onOpenChange={setBulkDeletePeopleOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Remover usuários selecionados</DialogTitle>
|
||||
<DialogDescription>Os usuários perderão o acesso imediatamente.</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="max-h-64 space-y-2 overflow-auto">
|
||||
{selectedPeopleUsers.slice(0, 5).map((u) => (
|
||||
<div key={`people-del-${u.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>
|
||||
))}
|
||||
{selectedPeopleUsers.length > 5 ? (
|
||||
<div className="px-3 text-xs text-neutral-500">+ {selectedPeopleUsers.length - 5} outros</div>
|
||||
) : null}
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setBulkDeletePeopleOpen(false)} disabled={isBulkDeletingPeople}>
|
||||
Cancelar
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
disabled={isBulkDeletingPeople}
|
||||
onClick={async () => {
|
||||
setIsBulkDeletingPeople(true)
|
||||
try {
|
||||
await performBulkDeleteUsers(selectedPeopleUsers.map((u) => u.id))
|
||||
setPeopleSelection(new Set())
|
||||
setBulkDeletePeopleOpen(false)
|
||||
toast.success("Remoção concluída")
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Falha ao excluir selecionados"
|
||||
toast.error(message)
|
||||
} finally {
|
||||
setIsBulkDeletingPeople(false)
|
||||
}
|
||||
}}
|
||||
>
|
||||
Excluir selecionados
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog open={bulkDeleteMachinesOpen} onOpenChange={setBulkDeleteMachinesOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Remover agentes selecionados</DialogTitle>
|
||||
<DialogDescription>As máquinas serão desconectadas e precisarão de novo provisionamento.</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="max-h-64 space-y-2 overflow-auto">
|
||||
{selectedMachineUsers.slice(0, 5).map((u) => (
|
||||
<div key={`machine-del-${u.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>
|
||||
))}
|
||||
{selectedMachineUsers.length > 5 ? (
|
||||
<div className="px-3 text-xs text-neutral-500">+ {selectedMachineUsers.length - 5} outros</div>
|
||||
) : null}
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setBulkDeleteMachinesOpen(false)} disabled={isBulkDeletingMachines}>
|
||||
Cancelar
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
disabled={isBulkDeletingMachines}
|
||||
onClick={async () => {
|
||||
setIsBulkDeletingMachines(true)
|
||||
try {
|
||||
await performBulkDeleteMachines(selectedMachineUsers.map((u) => u.id))
|
||||
setMachineSelection(new Set())
|
||||
setBulkDeleteMachinesOpen(false)
|
||||
toast.success("Agentes removidos")
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Falha ao excluir selecionados"
|
||||
toast.error(message)
|
||||
} finally {
|
||||
setIsBulkDeletingMachines(false)
|
||||
}
|
||||
}}
|
||||
>
|
||||
Remover selecionados
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
{/* Dialogs antigos removidos: ações em massa agora são unificadas no diálogo abaixo */}
|
||||
|
||||
<Dialog open={bulkRevokeInvitesOpen} onOpenChange={setBulkRevokeInvitesOpen}>
|
||||
<DialogContent>
|
||||
|
|
|
|||
|
|
@ -1934,7 +1934,7 @@ export function MachineDetails({ machine }: MachineDetailsProps) {
|
|||
>
|
||||
Adicionar vínculo
|
||||
</Button>
|
||||
<Link href="/admin" className="text-xs underline underline-offset-4">gerenciar usuários</Link>
|
||||
<Link href="/admin" className="text-xs underline underline-offset-4">Gerenciar usuários</Link>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
|
@ -3287,7 +3287,7 @@ function MachineCard({ machine, companyName }: { machine: MachinesQueryItem; com
|
|||
<span className="rounded-full border border-slate-300 bg-slate-100 px-2 py-0.5">
|
||||
Usuário vinculado: {collaborator.name ? `${collaborator.name} · ` : ""}{collaborator.email}
|
||||
</span>
|
||||
<Link href="/admin" className="underline underline-offset-4">gerenciar usuários</Link>
|
||||
<Link href="/admin" className="underline underline-offset-4">Gerenciar usuários</Link>
|
||||
</div>
|
||||
) : null}
|
||||
{!isActive ? (
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue