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()
|
.first()
|
||||||
if (!user) throw new ConvexError("Usuário não encontrado")
|
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)
|
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 }
|
return { ok: true }
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -246,19 +246,7 @@ export function AdminUsersManager({ initialUsers, initialInvites, roleOptions, d
|
||||||
const [teamSelection, setTeamSelection] = useState<Set<string>>(new Set())
|
const [teamSelection, setTeamSelection] = useState<Set<string>>(new Set())
|
||||||
const [isBulkDeletingTeam, setIsBulkDeletingTeam] = useState(false)
|
const [isBulkDeletingTeam, setIsBulkDeletingTeam] = useState(false)
|
||||||
const [bulkDeleteTeamOpen, setBulkDeleteTeamOpen] = useState(false)
|
const [bulkDeleteTeamOpen, setBulkDeleteTeamOpen] = useState(false)
|
||||||
// Usuários
|
// Removidos filtros antigos de Pessoas/Máquinas (agora unificado)
|
||||||
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)
|
|
||||||
// Unificado (pessoas + máquinas)
|
// Unificado (pessoas + máquinas)
|
||||||
const [usersSearch, setUsersSearch] = useState("")
|
const [usersSearch, setUsersSearch] = useState("")
|
||||||
const [usersTypeFilter, setUsersTypeFilter] = useState<"all" | "people" | "machines">("all")
|
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)
|
const [createPassword, setCreatePassword] = useState<string | null>(null)
|
||||||
|
|
||||||
// Máquinas (para listar vínculos por usuário)
|
// 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(
|
const machinesList = useQuery(
|
||||||
convexUserId ? api.machines.listByTenant : "skip",
|
convexUserId ? api.machines.listByTenant : "skip",
|
||||||
convexUserId ? { tenantId: defaultTenantId, includeMetadata: true } : ("skip" as const)
|
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
|
// Options of tenants present in dataset for filtering
|
||||||
const tenantOptions = useMemo(() => {
|
const tenantOptions = useMemo(() => {
|
||||||
|
|
@ -311,43 +334,9 @@ export function AdminUsersManager({ initialUsers, initialInvites, roleOptions, d
|
||||||
})
|
})
|
||||||
}, [teamUsers, teamSearch, teamRoleFilter, teamCompanyFilter, teamTenantFilter, defaultTenantId])
|
}, [teamUsers, teamSearch, teamRoleFilter, teamCompanyFilter, teamTenantFilter, defaultTenantId])
|
||||||
|
|
||||||
const filteredPeopleUsers = useMemo(() => {
|
// Removido: lista específica de Pessoas (uso substituído pelo unificado)
|
||||||
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])
|
|
||||||
|
|
||||||
const filteredMachineUsers = useMemo(() => {
|
// Removido: filtro específico de agentes (uso substituído pelo unificado)
|
||||||
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])
|
|
||||||
|
|
||||||
const combinedBaseUsers = useMemo(() => {
|
const combinedBaseUsers = useMemo(() => {
|
||||||
if (usersTypeFilter === "people") return peopleUsers
|
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()
|
if (typeof base.email === "string") collaboratorEmail = base.email.toLowerCase()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const linked = Array.isArray((m as any).linkedUsers)
|
const linked = Array.isArray(m.linkedUsers)
|
||||||
? ((m as any).linkedUsers as Array<{ email?: string }>).some((lu) => (lu.email ?? '').toLowerCase() === email)
|
? m.linkedUsers.some((lu) => (lu.email ?? '').toLowerCase() === email)
|
||||||
: false
|
: false
|
||||||
if (assigned === email || (collaboratorEmail && collaboratorEmail === email) || linked) {
|
if (assigned === email || (collaboratorEmail && collaboratorEmail === email) || linked) {
|
||||||
results.push({ id: m.id, hostname: m.hostname })
|
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 allTeamSelected = selectedTeamUsers.length > 0 && selectedTeamUsers.length === filteredTeamUsers.length
|
||||||
const someTeamSelected = selectedTeamUsers.length > 0 && !allTeamSelected
|
const someTeamSelected = selectedTeamUsers.length > 0 && !allTeamSelected
|
||||||
|
|
||||||
const selectedPeopleUsers = useMemo(() => filteredPeopleUsers.filter((u) => peopleSelection.has(u.id)), [filteredPeopleUsers, peopleSelection])
|
// Removido: seleção específica de Pessoas (uso substituído pelo unificado)
|
||||||
const allPeopleSelected = selectedPeopleUsers.length > 0 && selectedPeopleUsers.length === filteredPeopleUsers.length
|
|
||||||
const somePeopleSelected = selectedPeopleUsers.length > 0 && !allPeopleSelected
|
|
||||||
|
|
||||||
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 [inviteSelection, setInviteSelection] = useState<Set<string>>(new Set())
|
||||||
const selectedInvites = useMemo(() => invites.filter((i) => inviteSelection.has(i.id)), [invites, inviteSelection])
|
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) {
|
function toggleTeamSelectAll(checked: boolean) {
|
||||||
setTeamSelection(checked ? new Set(filteredTeamUsers.map((u) => u.id)) : new Set())
|
setTeamSelection(checked ? new Set(filteredTeamUsers.map((u) => u.id)) : new Set())
|
||||||
}
|
}
|
||||||
function togglePeopleSelectAll(checked: boolean) {
|
// Removidos: toggles de seleção específicos (uso substituído pelo unificado)
|
||||||
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())
|
|
||||||
}
|
|
||||||
function toggleInvitesSelectAll(checked: boolean) {
|
function toggleInvitesSelectAll(checked: boolean) {
|
||||||
setInviteSelection(checked ? new Set(invites.map((i) => i.id)) : new Set())
|
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">E-mail</th>
|
||||||
<th className="py-3 pr-4 font-medium">Papel</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">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">Espaço</th>
|
||||||
<th className="py-3 pr-4 font-medium">Criado em</th>
|
<th className="py-3 pr-4 font-medium">Criado em</th>
|
||||||
<th className="py-3 font-medium">Ações</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">{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">{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">
|
||||||
|
{(() => {
|
||||||
|
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-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>
|
||||||
<td className="py-3">
|
<td className="py-3">
|
||||||
|
|
@ -2012,93 +2003,7 @@ async function handleDeleteUser() {
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
<Dialog open={bulkDeletePeopleOpen} onOpenChange={setBulkDeletePeopleOpen}>
|
{/* Dialogs antigos removidos: ações em massa agora são unificadas no diálogo abaixo */}
|
||||||
<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>
|
|
||||||
|
|
||||||
<Dialog open={bulkRevokeInvitesOpen} onOpenChange={setBulkRevokeInvitesOpen}>
|
<Dialog open={bulkRevokeInvitesOpen} onOpenChange={setBulkRevokeInvitesOpen}>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
|
|
|
||||||
|
|
@ -1934,7 +1934,7 @@ export function MachineDetails({ machine }: MachineDetailsProps) {
|
||||||
>
|
>
|
||||||
Adicionar vínculo
|
Adicionar vínculo
|
||||||
</Button>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</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">
|
<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}
|
Usuário vinculado: {collaborator.name ? `${collaborator.name} · ` : ""}{collaborator.email}
|
||||||
</span>
|
</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>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
{!isActive ? (
|
{!isActive ? (
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue