Remove tenant UI; restrict machine links to non-admin users; polish Users/Machines UX
This commit is contained in:
parent
4a30a1b564
commit
347609a186
3 changed files with 54 additions and 87 deletions
|
|
@ -1088,6 +1088,10 @@ export const linkUser = mutation({
|
|||
.withIndex("by_tenant_email", (q) => q.eq("tenantId", tenantId).eq("email", normalized))
|
||||
.first()
|
||||
if (!user) throw new ConvexError("Usuário não encontrado")
|
||||
const role = (user.role ?? "").toUpperCase()
|
||||
if (role === 'ADMIN' || role === 'AGENT') {
|
||||
throw new ConvexError('Usuários administrativos não podem ser vinculados a máquinas')
|
||||
}
|
||||
|
||||
const current = new Set<Id<"users">>(machine.linkedUserIds ?? [])
|
||||
current.add(user._id)
|
||||
|
|
|
|||
|
|
@ -103,11 +103,7 @@ function machinePersonaBadgeVariant(persona: string | null | undefined) {
|
|||
return "outline" as const
|
||||
}
|
||||
|
||||
function formatTenantLabel(tenantId: string, defaultTenantId: string) {
|
||||
if (!tenantId) return "Principal"
|
||||
if (tenantId === defaultTenantId) return "Principal"
|
||||
return tenantId
|
||||
}
|
||||
// Tenant removido da UI (sem exibição)
|
||||
|
||||
function formatDate(dateIso: string) {
|
||||
const date = new Date(dateIso)
|
||||
|
|
@ -242,7 +238,7 @@ export function AdminUsersManager({ initialUsers, initialInvites, roleOptions, d
|
|||
const [teamSearch, setTeamSearch] = useState("")
|
||||
const [teamRoleFilter, setTeamRoleFilter] = useState<"all" | RoleOption>("all")
|
||||
const [teamCompanyFilter, setTeamCompanyFilter] = useState<string>("all")
|
||||
const [teamTenantFilter, setTeamTenantFilter] = useState<string>("all")
|
||||
// Removido: filtro por espaço (tenant)
|
||||
const [teamSelection, setTeamSelection] = useState<Set<string>>(new Set())
|
||||
const [isBulkDeletingTeam, setIsBulkDeletingTeam] = useState(false)
|
||||
const [bulkDeleteTeamOpen, setBulkDeleteTeamOpen] = useState(false)
|
||||
|
|
@ -251,7 +247,7 @@ export function AdminUsersManager({ initialUsers, initialInvites, roleOptions, d
|
|||
const [usersSearch, setUsersSearch] = useState("")
|
||||
const [usersTypeFilter, setUsersTypeFilter] = useState<"all" | "people" | "machines">("people")
|
||||
const [usersCompanyFilter, setUsersCompanyFilter] = useState<string>("all")
|
||||
const [usersTenantFilter, setUsersTenantFilter] = useState<string>("all")
|
||||
// Removido: filtro por espaço (tenant)
|
||||
const [usersSelection, setUsersSelection] = useState<Set<string>>(new Set())
|
||||
const [isBulkDeletingUsersCombined, setIsBulkDeletingUsersCombined] = useState(false)
|
||||
const [bulkDeleteUsersCombinedOpen, setBulkDeleteUsersCombinedOpen] = useState(false)
|
||||
|
|
@ -307,19 +303,13 @@ export function AdminUsersManager({ initialUsers, initialInvites, roleOptions, d
|
|||
return map
|
||||
}, [machinesList])
|
||||
|
||||
// Options of tenants present in dataset for filtering
|
||||
const tenantOptions = useMemo(() => {
|
||||
const set = new Set<string>()
|
||||
users.forEach((u) => u.tenantId && set.add(u.tenantId))
|
||||
const list = Array.from(set)
|
||||
return list.length > 0 ? list : [defaultTenantId]
|
||||
}, [users, defaultTenantId])
|
||||
// Tenant removido da UI
|
||||
|
||||
const filteredTeamUsers = useMemo(() => {
|
||||
const term = teamSearch.trim().toLowerCase()
|
||||
return teamUsers.filter((user) => {
|
||||
if (teamCompanyFilter !== "all" && user.companyId !== teamCompanyFilter) return false
|
||||
if (teamTenantFilter !== "all" && (user.tenantId ?? defaultTenantId) !== teamTenantFilter) return false
|
||||
// filtro por espaço removido
|
||||
if (teamRoleFilter !== "all" && coerceRole(user.role) !== teamRoleFilter) return false
|
||||
if (!term) return true
|
||||
const haystack = [
|
||||
|
|
@ -332,7 +322,7 @@ export function AdminUsersManager({ initialUsers, initialInvites, roleOptions, d
|
|||
.toLowerCase()
|
||||
return haystack.includes(term)
|
||||
})
|
||||
}, [teamUsers, teamSearch, teamRoleFilter, teamCompanyFilter, teamTenantFilter, defaultTenantId])
|
||||
}, [teamUsers, teamSearch, teamRoleFilter, teamCompanyFilter])
|
||||
|
||||
// Removido: lista específica de Pessoas (uso substituído pelo unificado)
|
||||
|
||||
|
|
@ -348,7 +338,7 @@ export function AdminUsersManager({ initialUsers, initialInvites, roleOptions, d
|
|||
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
|
||||
// filtro por espaço removido
|
||||
if (!term) return true
|
||||
const persona = (user.machinePersona ?? "").toLowerCase()
|
||||
const machineId = extractMachineId(user.email) ?? ""
|
||||
|
|
@ -364,7 +354,7 @@ export function AdminUsersManager({ initialUsers, initialInvites, roleOptions, d
|
|||
.toLowerCase()
|
||||
return haystack.includes(term)
|
||||
})
|
||||
}, [combinedBaseUsers, usersSearch, usersCompanyFilter, usersTenantFilter, defaultTenantId])
|
||||
}, [combinedBaseUsers, usersSearch, usersCompanyFilter])
|
||||
|
||||
useEffect(() => {
|
||||
void (async () => {
|
||||
|
|
@ -987,17 +977,7 @@ async function handleDeleteUser() {
|
|||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Select value={teamTenantFilter} onValueChange={setTeamTenantFilter}>
|
||||
<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={`team-tenant-${t}`} value={t}>{formatTenantLabel(t, defaultTenantId)}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{/* Filtro por espaço removido */}
|
||||
{(teamSearch.trim().length > 0 || teamRoleFilter !== "all") ? (
|
||||
<Button
|
||||
variant="ghost"
|
||||
|
|
@ -1006,7 +986,7 @@ async function handleDeleteUser() {
|
|||
setTeamSearch("")
|
||||
setTeamRoleFilter("all")
|
||||
setTeamCompanyFilter("all")
|
||||
setTeamTenantFilter("all")
|
||||
// filtro por espaço removido
|
||||
}}
|
||||
>
|
||||
Limpar filtros
|
||||
|
|
@ -1046,7 +1026,7 @@ async function handleDeleteUser() {
|
|||
<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>
|
||||
{/* Espaço removido */}
|
||||
<th className="py-3 pr-4 font-medium">Criado em</th>
|
||||
<th className="py-3 font-medium">Ações</th>
|
||||
</tr>
|
||||
|
|
@ -1077,12 +1057,14 @@ async function handleDeleteUser() {
|
|||
<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>
|
||||
) : '—'
|
||||
if (list.length === 0) return '—'
|
||||
const names = list.map((m) => m.hostname || m.id)
|
||||
const head = names.slice(0, 2).join(', ')
|
||||
const extra = names.length > 2 ? ` +${names.length - 2}` : ''
|
||||
return <span className="text-xs font-medium" title={names.join(', ')}>{head}{extra}</span>
|
||||
})()}
|
||||
</td>
|
||||
<td className="py-3 pr-4 text-neutral-600">{formatTenantLabel(user.tenantId, defaultTenantId)}</td>
|
||||
{/* Espaço removido */}
|
||||
<td className="py-3 pr-4 text-neutral-500">{formatDate(user.createdAt)}</td>
|
||||
<td className="py-3">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
|
|
@ -1213,18 +1195,8 @@ async function handleDeleteUser() {
|
|||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<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={`users-tenant-${t}`} value={t}>{formatTenantLabel(t, defaultTenantId)}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{(usersSearch.trim().length > 0 || usersTypeFilter !== "all" || usersCompanyFilter !== "all" || usersTenantFilter !== "all") ? (
|
||||
{/* Filtro por espaço removido */}
|
||||
{(usersSearch.trim().length > 0 || usersTypeFilter !== "all" || usersCompanyFilter !== "all") ? (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
|
|
@ -1232,7 +1204,7 @@ async function handleDeleteUser() {
|
|||
setUsersSearch("")
|
||||
setUsersTypeFilter("all")
|
||||
setUsersCompanyFilter("all")
|
||||
setUsersTenantFilter("all")
|
||||
// filtro por espaço removido
|
||||
}}
|
||||
>
|
||||
Limpar filtros
|
||||
|
|
@ -1272,7 +1244,7 @@ async function handleDeleteUser() {
|
|||
<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>
|
||||
{/* Espaço removido */}
|
||||
<th className="py-3 pr-4 font-medium">Criado em</th>
|
||||
<th className="py-3 font-medium">Ações</th>
|
||||
</tr>
|
||||
|
|
@ -1313,7 +1285,7 @@ async function handleDeleteUser() {
|
|||
)}
|
||||
</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>
|
||||
{/* Espaço removido */}
|
||||
<td className="py-3 pr-4 text-neutral-500">{formatDate(user.createdAt)}</td>
|
||||
<td className="py-3">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
|
|
@ -1465,7 +1437,7 @@ async function handleDeleteUser() {
|
|||
</th>
|
||||
<th className="py-3 pr-4 font-medium">Colaborador</th>
|
||||
<th className="py-3 pr-4 font-medium">Papel</th>
|
||||
<th className="py-3 pr-4 font-medium">Espaço</th>
|
||||
{/* Espaço removido */}
|
||||
<th className="py-3 pr-4 font-medium">Expira em</th>
|
||||
<th className="py-3 pr-4 font-medium">Status</th>
|
||||
<th className="py-3 font-medium">Ações</th>
|
||||
|
|
@ -1497,7 +1469,7 @@ async function handleDeleteUser() {
|
|||
</div>
|
||||
</td>
|
||||
<td className="py-3 pr-4 text-neutral-600">{formatRole(invite.role)}</td>
|
||||
<td className="py-3 pr-4 text-neutral-600">{formatTenantLabel(invite.tenantId, defaultTenantId)}</td>
|
||||
{/* Espaço removido */}
|
||||
<td className="py-3 pr-4 text-neutral-600">{formatDate(invite.expiresAt)}</td>
|
||||
<td className="py-3 pr-4">
|
||||
<Badge
|
||||
|
|
@ -1620,15 +1592,7 @@ async function handleDeleteUser() {
|
|||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="create-tenant">Espaço (tenant)</Label>
|
||||
<Input
|
||||
id="create-tenant"
|
||||
placeholder="tenant-atlas"
|
||||
value={createForm.tenantId}
|
||||
onChange={(event) => setCreateForm((prev) => ({ ...prev, tenantId: event.target.value }))}
|
||||
/>
|
||||
</div>
|
||||
{/* Campo de espaço (tenant) removido */}
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="create-company">Empresa vinculada</Label>
|
||||
<Select
|
||||
|
|
@ -1778,15 +1742,7 @@ async function handleDeleteUser() {
|
|||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label>Espaço (tenant)</Label>
|
||||
<Input
|
||||
value={editForm.tenantId}
|
||||
onChange={(event) => setEditForm((prev) => ({ ...prev, tenantId: event.target.value }))}
|
||||
placeholder="tenant-atlas"
|
||||
disabled={isSavingUser || isMachineEditing || editingRestricted}
|
||||
/>
|
||||
</div>
|
||||
{/* Campo de espaço (tenant) removido */}
|
||||
<div className="grid gap-2">
|
||||
<Label>Empresa vinculada</Label>
|
||||
<Select
|
||||
|
|
@ -1809,6 +1765,10 @@ async function handleDeleteUser() {
|
|||
</Select>
|
||||
<p className="text-xs text-neutral-500">Essa seleção substitui o vínculo atual no portal do cliente.</p>
|
||||
</div>
|
||||
{(() => {
|
||||
const r = coerceRole(editUser.role)
|
||||
if (r === 'admin' || r === 'agent') return null
|
||||
return (
|
||||
<div className="grid gap-2">
|
||||
<Label>Máquinas vinculadas</Label>
|
||||
{linkedMachinesForEditUser.length > 0 ? (
|
||||
|
|
@ -1826,6 +1786,8 @@ async function handleDeleteUser() {
|
|||
<p className="text-xs text-neutral-500">Nenhuma máquina vinculada a este usuário.</p>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})()}
|
||||
{isMachineEditing ? (
|
||||
<div className="rounded-lg border border-dashed border-slate-300 bg-slate-50 p-3 text-sm text-neutral-600">
|
||||
Os ajustes detalhados de agentes de máquina são feitos em <Link href="/admin/machines" className="underline underline-offset-4">Admin ▸ Máquinas</Link>.
|
||||
|
|
|
|||
|
|
@ -1934,6 +1934,7 @@ export function MachineDetails({ machine }: MachineDetailsProps) {
|
|||
>
|
||||
Adicionar vínculo
|
||||
</Button>
|
||||
<span className="text-xs text-neutral-500">Somente colaboradores/gestores.</span>
|
||||
<Link href="/admin" className="text-xs underline underline-offset-4">Gerenciar usuários</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue