admin: split Equipe/Usuários, add bulk select/actions for users, machines and invites; add company/tenant filters
This commit is contained in:
parent
515d1718a6
commit
a325d612cb
1 changed files with 641 additions and 16 deletions
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
import Link from "next/link"
|
import Link from "next/link"
|
||||||
import { useCallback, useEffect, useMemo, useState, useTransition } from "react"
|
import { useCallback, useEffect, useMemo, useState, useTransition } from "react"
|
||||||
import { IconSearch, IconUserPlus } from "@tabler/icons-react"
|
import { IconSearch, IconUserPlus, IconTrash } from "@tabler/icons-react"
|
||||||
|
|
||||||
import { toast } from "sonner"
|
import { toast } from "sonner"
|
||||||
|
|
||||||
|
|
@ -12,6 +12,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/com
|
||||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"
|
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"
|
||||||
import { Input } from "@/components/ui/input"
|
import { Input } from "@/components/ui/input"
|
||||||
import { Label } from "@/components/ui/label"
|
import { Label } from "@/components/ui/label"
|
||||||
|
import { Checkbox } from "@/components/ui/checkbox"
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
|
|
@ -221,14 +222,39 @@ export function AdminUsersManager({ initialUsers, initialInvites, roleOptions, d
|
||||||
})
|
})
|
||||||
return Array.from(unique)
|
return Array.from(unique)
|
||||||
}, [normalizedRoles, viewerIsAdmin])
|
}, [normalizedRoles, viewerIsAdmin])
|
||||||
const teamUsers = useMemo(() => users.filter((user) => user.role !== "machine"), [users])
|
// Split users: team (admin/agent) and people (manager/collaborator); exclude machines
|
||||||
|
const teamUsers = useMemo(
|
||||||
|
() => users.filter((user) => user.role !== "machine" && ["admin", "agent"].includes(coerceRole(user.role))),
|
||||||
|
[users],
|
||||||
|
)
|
||||||
|
const peopleUsers = useMemo(
|
||||||
|
() => users.filter((user) => user.role !== "machine" && ["manager", "collaborator"].includes(coerceRole(user.role))),
|
||||||
|
[users],
|
||||||
|
)
|
||||||
const machineUsers = useMemo(() => users.filter((user) => user.role === "machine"), [users])
|
const machineUsers = useMemo(() => users.filter((user) => user.role === "machine"), [users])
|
||||||
|
|
||||||
const defaultCreateRole: RoleOption = selectableRoles[0] ?? "agent"
|
const defaultCreateRole: RoleOption = selectableRoles[0] ?? "agent"
|
||||||
|
// Equipe
|
||||||
const [teamSearch, setTeamSearch] = useState("")
|
const [teamSearch, setTeamSearch] = useState("")
|
||||||
const [teamRoleFilter, setTeamRoleFilter] = useState<"all" | RoleOption>("all")
|
const [teamRoleFilter, setTeamRoleFilter] = useState<"all" | RoleOption>("all")
|
||||||
|
const [teamCompanyFilter, setTeamCompanyFilter] = useState<string>("all")
|
||||||
|
const [teamTenantFilter, setTeamTenantFilter] = useState<string>("all")
|
||||||
|
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 [machineSearch, setMachineSearch] = useState("")
|
||||||
const [machinePersonaFilter, setMachinePersonaFilter] = useState<"all" | "manager" | "collaborator" | "unassigned">("all")
|
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)
|
||||||
const [createDialogOpen, setCreateDialogOpen] = useState(false)
|
const [createDialogOpen, setCreateDialogOpen] = useState(false)
|
||||||
const [createForm, setCreateForm] = useState({
|
const [createForm, setCreateForm] = useState({
|
||||||
name: "",
|
name: "",
|
||||||
|
|
@ -240,9 +266,19 @@ export function AdminUsersManager({ initialUsers, initialInvites, roleOptions, d
|
||||||
const [isCreatingUser, setIsCreatingUser] = useState(false)
|
const [isCreatingUser, setIsCreatingUser] = useState(false)
|
||||||
const [createPassword, setCreatePassword] = useState<string | null>(null)
|
const [createPassword, setCreatePassword] = useState<string | null>(null)
|
||||||
|
|
||||||
|
// 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])
|
||||||
|
|
||||||
const filteredTeamUsers = useMemo(() => {
|
const filteredTeamUsers = useMemo(() => {
|
||||||
const term = teamSearch.trim().toLowerCase()
|
const term = teamSearch.trim().toLowerCase()
|
||||||
return teamUsers.filter((user) => {
|
return teamUsers.filter((user) => {
|
||||||
|
if (teamCompanyFilter !== "all" && user.companyId !== teamCompanyFilter) return false
|
||||||
|
if (teamTenantFilter !== "all" && (user.tenantId ?? defaultTenantId) !== teamTenantFilter) return false
|
||||||
if (teamRoleFilter !== "all" && coerceRole(user.role) !== teamRoleFilter) return false
|
if (teamRoleFilter !== "all" && coerceRole(user.role) !== teamRoleFilter) return false
|
||||||
if (!term) return true
|
if (!term) return true
|
||||||
const haystack = [
|
const haystack = [
|
||||||
|
|
@ -255,7 +291,27 @@ export function AdminUsersManager({ initialUsers, initialInvites, roleOptions, d
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
return haystack.includes(term)
|
return haystack.includes(term)
|
||||||
})
|
})
|
||||||
}, [teamUsers, teamSearch, teamRoleFilter])
|
}, [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])
|
||||||
|
|
||||||
const filteredMachineUsers = useMemo(() => {
|
const filteredMachineUsers = useMemo(() => {
|
||||||
const term = machineSearch.trim().toLowerCase()
|
const term = machineSearch.trim().toLowerCase()
|
||||||
|
|
@ -582,6 +638,97 @@ export function AdminUsersManager({ initialUsers, initialInvites, roleOptions, d
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Bulk selection helpers
|
||||||
|
const selectedTeamUsers = useMemo(() => filteredTeamUsers.filter((u) => teamSelection.has(u.id)), [filteredTeamUsers, teamSelection])
|
||||||
|
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
|
||||||
|
|
||||||
|
const selectedMachineUsers = useMemo(() => filteredMachineUsers.filter((u) => machineSelection.has(u.id)), [filteredMachineUsers, machineSelection])
|
||||||
|
const allMachinesSelected = selectedMachineUsers.length > 0 && selectedMachineUsers.length === filteredMachineUsers.length
|
||||||
|
const someMachinesSelected = selectedMachineUsers.length > 0 && !allMachinesSelected
|
||||||
|
|
||||||
|
const [inviteSelection, setInviteSelection] = useState<Set<string>>(new Set())
|
||||||
|
const selectedInvites = useMemo(() => invites.filter((i) => inviteSelection.has(i.id)), [invites, inviteSelection])
|
||||||
|
const allInvitesSelected = selectedInvites.length > 0 && selectedInvites.length === invites.length
|
||||||
|
const someInvitesSelected = selectedInvites.length > 0 && !allInvitesSelected
|
||||||
|
const [isBulkRevokingInvites, setIsBulkRevokingInvites] = useState(false)
|
||||||
|
const [bulkRevokeInvitesOpen, setBulkRevokeInvitesOpen] = useState(false)
|
||||||
|
|
||||||
|
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())
|
||||||
|
}
|
||||||
|
function toggleInvitesSelectAll(checked: boolean) {
|
||||||
|
setInviteSelection(checked ? new Set(invites.map((i) => i.id)) : new Set())
|
||||||
|
}
|
||||||
|
|
||||||
|
async function performBulkDeleteUsers(ids: string[]) {
|
||||||
|
if (ids.length === 0) return
|
||||||
|
const tasks = ids.map(async (id) => {
|
||||||
|
const user = users.find((u) => u.id === id)
|
||||||
|
if (!user) return
|
||||||
|
if (!canManageUser(user.role)) return
|
||||||
|
const response = await fetch(`/api/admin/users/${id}`, { method: "DELETE", credentials: "include" })
|
||||||
|
if (!response.ok) {
|
||||||
|
const data = await response.json().catch(() => ({}))
|
||||||
|
throw new Error(data.error ?? `Falha ao remover ${user.email}`)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
await Promise.allSettled(tasks)
|
||||||
|
setUsers((prev) => prev.filter((u) => !ids.includes(u.id)))
|
||||||
|
}
|
||||||
|
|
||||||
|
async function performBulkDeleteMachines(ids: string[]) {
|
||||||
|
if (ids.length === 0) return
|
||||||
|
const tasks = ids.map(async (id) => {
|
||||||
|
const user = users.find((u) => u.id === id)
|
||||||
|
if (!user) return
|
||||||
|
const machineId = extractMachineId(user.email)
|
||||||
|
if (!machineId) return
|
||||||
|
const response = await fetch(`/api/admin/machines/delete`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
credentials: "include",
|
||||||
|
body: JSON.stringify({ machineId }),
|
||||||
|
})
|
||||||
|
if (!response.ok) {
|
||||||
|
const data = await response.json().catch(() => ({}))
|
||||||
|
throw new Error(data.error ?? `Falha ao remover agente ${user.email}`)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
await Promise.allSettled(tasks)
|
||||||
|
setUsers((prev) => prev.filter((u) => !ids.includes(u.id)))
|
||||||
|
}
|
||||||
|
|
||||||
|
async function performBulkRevokeInvites(ids: string[]) {
|
||||||
|
if (ids.length === 0) return
|
||||||
|
const tasks = ids.map(async (id) => {
|
||||||
|
const invite = invites.find((i) => i.id === id)
|
||||||
|
if (!invite) return
|
||||||
|
if (!canManageInvite(invite.role)) return
|
||||||
|
const response = await fetch(`/api/admin/invites/${id}`, {
|
||||||
|
method: "PATCH",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ reason: "Revogado em massa" }),
|
||||||
|
})
|
||||||
|
if (!response.ok) {
|
||||||
|
const data = await response.json().catch(() => ({}))
|
||||||
|
throw new Error(data.error ?? `Falha ao revogar ${invite.email}`)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
await Promise.allSettled(tasks)
|
||||||
|
setInvites((prev) => prev.map((i) => (ids.includes(i.id) && i.status === "pending" ? { ...i, status: "revoked", revokedAt: new Date().toISOString() } : i)))
|
||||||
|
}
|
||||||
|
|
||||||
async function handleSaveUser(event: React.FormEvent<HTMLFormElement>) {
|
async function handleSaveUser(event: React.FormEvent<HTMLFormElement>) {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
if (!editUser) return
|
if (!editUser) return
|
||||||
|
|
@ -718,20 +865,19 @@ async function handleDeleteUser() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Tabs defaultValue="users" className="w-full">
|
<Tabs defaultValue="team" className="w-full">
|
||||||
<TabsList className="h-12 w-full justify-start rounded-xl bg-slate-100 p-1">
|
<TabsList className="h-12 w-full justify-start rounded-xl bg-slate-100 p-1">
|
||||||
<TabsTrigger value="users" className="rounded-lg">Equipe</TabsTrigger>
|
<TabsTrigger value="team" className="rounded-lg">Equipe</TabsTrigger>
|
||||||
|
<TabsTrigger value="people" className="rounded-lg">Usuários</TabsTrigger>
|
||||||
<TabsTrigger value="machines" className="rounded-lg">Agentes de máquina</TabsTrigger>
|
<TabsTrigger value="machines" className="rounded-lg">Agentes de máquina</TabsTrigger>
|
||||||
<TabsTrigger value="invites" className="rounded-lg">Convites</TabsTrigger>
|
<TabsTrigger value="invites" className="rounded-lg">Convites</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
|
|
||||||
<TabsContent value="users" className="mt-6 space-y-6">
|
<TabsContent value="team" className="mt-6 space-y-6">
|
||||||
<div className="flex flex-col gap-4 rounded-xl border border-slate-200 bg-white p-4 shadow-sm sm:flex-row sm:items-center sm:justify-between">
|
<div className="flex flex-col gap-4 rounded-xl border border-slate-200 bg-white p-4 shadow-sm sm:flex-row sm:items-center sm:justify-between">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<p className="text-sm font-semibold text-neutral-900">Equipe cadastrada</p>
|
<p className="text-sm font-semibold text-neutral-900">Equipe cadastrada</p>
|
||||||
<p className="text-xs text-neutral-500">
|
<p className="text-xs text-neutral-500">{filteredTeamUsers.length} {filteredTeamUsers.length === 1 ? "membro" : "membros"}</p>
|
||||||
{filteredTeamUsers.length} {filteredTeamUsers.length === 1 ? "colaborador" : "colaboradores"}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<Button onClick={handleOpenCreateUser} size="sm" className="gap-2 self-start sm:self-auto">
|
<Button onClick={handleOpenCreateUser} size="sm" className="gap-2 self-start sm:self-auto">
|
||||||
<IconUserPlus className="size-4" />
|
<IconUserPlus className="size-4" />
|
||||||
|
|
@ -755,10 +901,30 @@ async function handleDeleteUser() {
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="all">Todos os papéis</SelectItem>
|
<SelectItem value="all">Todos os papéis</SelectItem>
|
||||||
{selectableRoles.map((option) => (
|
{selectableRoles.filter((option) => ["admin", "agent"].includes(option)).map((option) => (
|
||||||
<SelectItem key={`team-filter-${option}`} value={option}>
|
<SelectItem key={`team-filter-${option}`} value={option}>{formatRole(option)}</SelectItem>
|
||||||
{formatRole(option)}
|
))}
|
||||||
</SelectItem>
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<Select value={teamCompanyFilter} onValueChange={setTeamCompanyFilter}>
|
||||||
|
<SelectTrigger className="h-9 w-full sm:w-56">
|
||||||
|
<SelectValue placeholder="Empresa" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="all">Todas as empresas</SelectItem>
|
||||||
|
{companies.map((company) => (
|
||||||
|
<SelectItem key={`team-company-${company.id}`} value={company.id}>{company.name}</SelectItem>
|
||||||
|
))}
|
||||||
|
</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>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
|
|
@ -769,11 +935,22 @@ async function handleDeleteUser() {
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setTeamSearch("")
|
setTeamSearch("")
|
||||||
setTeamRoleFilter("all")
|
setTeamRoleFilter("all")
|
||||||
|
setTeamCompanyFilter("all")
|
||||||
|
setTeamTenantFilter("all")
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Limpar filtros
|
Limpar filtros
|
||||||
</Button>
|
</Button>
|
||||||
) : null}
|
) : null}
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="gap-2 border-rose-200 text-rose-600 hover:bg-rose-50 hover:text-rose-700 disabled:text-rose-300 disabled:border-rose-100"
|
||||||
|
disabled={selectedTeamUsers.length === 0 || isBulkDeletingTeam}
|
||||||
|
onClick={() => setBulkDeleteTeamOpen(true)}
|
||||||
|
>
|
||||||
|
<IconTrash className="size-4" /> Excluir selecionados
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Card>
|
<Card>
|
||||||
|
|
@ -785,6 +962,15 @@ async function handleDeleteUser() {
|
||||||
<table className="min-w-full table-fixed divide-y divide-slate-200 text-sm">
|
<table className="min-w-full table-fixed divide-y divide-slate-200 text-sm">
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="text-left text-xs uppercase tracking-wide text-neutral-500">
|
<tr className="text-left text-xs uppercase tracking-wide text-neutral-500">
|
||||||
|
<th className="w-10 py-3 pr-2 font-medium">
|
||||||
|
<div className="flex items-center justify-center">
|
||||||
|
<Checkbox
|
||||||
|
checked={allTeamSelected || (someTeamSelected && "indeterminate")}
|
||||||
|
onCheckedChange={(value) => toggleTeamSelectAll(!!value)}
|
||||||
|
aria-label="Selecionar todos"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</th>
|
||||||
<th className="py-3 pr-4 font-medium">Nome</th>
|
<th className="py-3 pr-4 font-medium">Nome</th>
|
||||||
<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>
|
||||||
|
|
@ -797,6 +983,22 @@ async function handleDeleteUser() {
|
||||||
<tbody className="divide-y divide-slate-100">
|
<tbody className="divide-y divide-slate-100">
|
||||||
{filteredTeamUsers.map((user) => (
|
{filteredTeamUsers.map((user) => (
|
||||||
<tr key={user.id} className="hover:bg-slate-50">
|
<tr key={user.id} className="hover:bg-slate-50">
|
||||||
|
<td className="py-3 pr-2">
|
||||||
|
<div className="flex items-center justify-center">
|
||||||
|
<Checkbox
|
||||||
|
checked={teamSelection.has(user.id)}
|
||||||
|
onCheckedChange={(checked) => {
|
||||||
|
setTeamSelection((prev) => {
|
||||||
|
const next = new Set(prev)
|
||||||
|
if (checked) next.add(user.id)
|
||||||
|
else next.delete(user.id)
|
||||||
|
return next
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
aria-label="Selecionar linha"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
<td className="py-3 pr-4 font-medium text-neutral-800">{user.name || "—"}</td>
|
<td className="py-3 pr-4 font-medium text-neutral-800">{user.name || "—"}</td>
|
||||||
<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>
|
||||||
|
|
@ -834,7 +1036,7 @@ async function handleDeleteUser() {
|
||||||
))}
|
))}
|
||||||
{filteredTeamUsers.length === 0 ? (
|
{filteredTeamUsers.length === 0 ? (
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={7} className="py-6 text-center text-neutral-500">
|
<td colSpan={8} className="py-6 text-center text-neutral-500">
|
||||||
{teamUsers.length === 0
|
{teamUsers.length === 0
|
||||||
? "Nenhum usuário cadastrado até o momento."
|
? "Nenhum usuário cadastrado até o momento."
|
||||||
: "Nenhum usuário corresponde aos filtros atuais."}
|
: "Nenhum usuário corresponde aos filtros atuais."}
|
||||||
|
|
@ -889,6 +1091,181 @@ async function handleDeleteUser() {
|
||||||
</Card>
|
</Card>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="people" className="mt-6 space-y-6">
|
||||||
|
<div className="flex flex-col gap-4 rounded-xl border border-slate-200 bg-white p-4 shadow-sm sm:flex-row sm:items-center sm:justify-between">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<p className="text-sm font-semibold text-neutral-900">Usuários</p>
|
||||||
|
<p className="text-xs text-neutral-500">{filteredPeopleUsers.length} {filteredPeopleUsers.length === 1 ? "usuário" : "usuários"}</p>
|
||||||
|
</div>
|
||||||
|
<Button onClick={handleOpenCreateUser} size="sm" className="gap-2 self-start sm:self-auto">
|
||||||
|
<IconUserPlus className="size-4" />
|
||||||
|
Novo usuário
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-3 rounded-xl border border-slate-200 bg-white p-4 shadow-sm sm:flex-row sm:items-center sm:justify-between">
|
||||||
|
<div className="relative w-full sm:max-w-xs">
|
||||||
|
<IconSearch className="text-muted-foreground pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2" />
|
||||||
|
<Input
|
||||||
|
value={peopleSearch}
|
||||||
|
onChange={(event) => setPeopleSearch(event.target.value)}
|
||||||
|
placeholder="Buscar por nome, e-mail ou empresa..."
|
||||||
|
className="h-9 pl-9"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap items-center gap-3">
|
||||||
|
<Select value={peopleRoleFilter} onValueChange={(value) => setPeopleRoleFilter(value as "all" | "manager" | "collaborator")}>
|
||||||
|
<SelectTrigger className="h-9 w-full sm:w-48">
|
||||||
|
<SelectValue placeholder="Perfil" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="all">Todos os perfis</SelectItem>
|
||||||
|
<SelectItem value="manager">Gestores</SelectItem>
|
||||||
|
<SelectItem value="collaborator">Colaboradores</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<Select value={peopleCompanyFilter} onValueChange={setPeopleCompanyFilter}>
|
||||||
|
<SelectTrigger className="h-9 w-full sm:w-56">
|
||||||
|
<SelectValue placeholder="Empresa" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="all">Todas as empresas</SelectItem>
|
||||||
|
{companies.map((company) => (
|
||||||
|
<SelectItem key={`people-company-${company.id}`} value={company.id}>{company.name}</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<Select value={peopleTenantFilter} onValueChange={setPeopleTenantFilter}>
|
||||||
|
<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={`people-tenant-${t}`} value={t}>{formatTenantLabel(t, defaultTenantId)}</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
{(peopleSearch.trim().length > 0 || peopleRoleFilter !== "all" || peopleCompanyFilter !== "all" || peopleTenantFilter !== "all") ? (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => {
|
||||||
|
setPeopleSearch("")
|
||||||
|
setPeopleRoleFilter("all")
|
||||||
|
setPeopleCompanyFilter("all")
|
||||||
|
setPeopleTenantFilter("all")
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Limpar filtros
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="gap-2 border-rose-200 text-rose-600 hover:bg-rose-50 hover:text-rose-700 disabled:text-rose-300 disabled:border-rose-100"
|
||||||
|
disabled={selectedPeopleUsers.length === 0 || isBulkDeletingPeople}
|
||||||
|
onClick={() => setBulkDeletePeopleOpen(true)}
|
||||||
|
>
|
||||||
|
<IconTrash className="size-4" /> Excluir selecionados
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Usuários</CardTitle>
|
||||||
|
<CardDescription>Colaboradores e gestores vinculados às empresas.</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="overflow-x-auto">
|
||||||
|
<table className="min-w-full table-fixed divide-y divide-slate-200 text-sm">
|
||||||
|
<thead>
|
||||||
|
<tr className="text-left text-xs uppercase tracking-wide text-neutral-500">
|
||||||
|
<th className="w-10 py-3 pr-2 font-medium">
|
||||||
|
<div className="flex items-center justify-center">
|
||||||
|
<Checkbox
|
||||||
|
checked={allPeopleSelected || (somePeopleSelected && "indeterminate")}
|
||||||
|
onCheckedChange={(value) => togglePeopleSelectAll(!!value)}
|
||||||
|
aria-label="Selecionar todos"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</th>
|
||||||
|
<th className="py-3 pr-4 font-medium">Nome</th>
|
||||||
|
<th className="py-3 pr-4 font-medium">E-mail</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>
|
||||||
|
<th className="py-3 pr-4 font-medium">Criado em</th>
|
||||||
|
<th className="py-3 font-medium">Ações</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="divide-y divide-slate-100">
|
||||||
|
{filteredPeopleUsers.map((user) => (
|
||||||
|
<tr key={user.id} className="hover:bg-slate-50">
|
||||||
|
<td className="py-3 pr-2">
|
||||||
|
<div className="flex items-center justify-center">
|
||||||
|
<Checkbox
|
||||||
|
checked={peopleSelection.has(user.id)}
|
||||||
|
onCheckedChange={(checked) => {
|
||||||
|
setPeopleSelection((prev) => {
|
||||||
|
const next = new Set(prev)
|
||||||
|
if (checked) next.add(user.id)
|
||||||
|
else next.delete(user.id)
|
||||||
|
return next
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
aria-label="Selecionar linha"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="py-3 pr-4 font-medium text-neutral-800">{user.name || "—"}</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">{user.companyName ?? "—"}</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">
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
disabled={!canManageUser(user.role)}
|
||||||
|
onClick={() => {
|
||||||
|
if (!canManageUser(user.role)) return
|
||||||
|
setEditUserId(user.id)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Editar
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="border-rose-200 text-rose-600 hover:bg-rose-50 hover:text-rose-700"
|
||||||
|
disabled={!canManageUser(user.role)}
|
||||||
|
onClick={() => {
|
||||||
|
if (!canManageUser(user.role)) return
|
||||||
|
setDeleteUserId(user.id)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Remover
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
{filteredPeopleUsers.length === 0 ? (
|
||||||
|
<tr>
|
||||||
|
<td colSpan={8} className="py-6 text-center text-neutral-500">
|
||||||
|
{peopleUsers.length === 0
|
||||||
|
? "Nenhum usuário cadastrado até o momento."
|
||||||
|
: "Nenhum usuário corresponde aos filtros atuais."}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
) : null}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="machines" className="mt-6 space-y-6">
|
<TabsContent value="machines" className="mt-6 space-y-6">
|
||||||
<div className="rounded-xl border border-slate-200 bg-white p-4 shadow-sm">
|
<div className="rounded-xl border border-slate-200 bg-white p-4 shadow-sm">
|
||||||
<div className="flex flex-col gap-1 sm:flex-row sm:items-center sm:justify-between">
|
<div className="flex flex-col gap-1 sm:flex-row sm:items-center sm:justify-between">
|
||||||
|
|
@ -932,6 +1309,15 @@ async function handleDeleteUser() {
|
||||||
Limpar filtros
|
Limpar filtros
|
||||||
</Button>
|
</Button>
|
||||||
) : null}
|
) : null}
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="gap-2 border-rose-200 text-rose-600 hover:bg-rose-50 hover:text-rose-700 disabled:text-rose-300 disabled:border-rose-100"
|
||||||
|
disabled={selectedMachineUsers.length === 0 || isBulkDeletingMachines}
|
||||||
|
onClick={() => setBulkDeleteMachinesOpen(true)}
|
||||||
|
>
|
||||||
|
<IconTrash className="size-4" /> Remover selecionados
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Card>
|
<Card>
|
||||||
|
|
@ -943,6 +1329,15 @@ async function handleDeleteUser() {
|
||||||
<table className="min-w-full table-fixed divide-y divide-slate-200 text-sm">
|
<table className="min-w-full table-fixed divide-y divide-slate-200 text-sm">
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="text-left text-xs uppercase tracking-wide text-neutral-500">
|
<tr className="text-left text-xs uppercase tracking-wide text-neutral-500">
|
||||||
|
<th className="w-10 py-3 pr-2 font-medium">
|
||||||
|
<div className="flex items-center justify-center">
|
||||||
|
<Checkbox
|
||||||
|
checked={selectedMachineUsers.length > 0 && selectedMachineUsers.length === filteredMachineUsers.length || (selectedMachineUsers.length > 0 && selectedMachineUsers.length < filteredMachineUsers.length && "indeterminate")}
|
||||||
|
onCheckedChange={(value) => toggleMachinesSelectAll(!!value)}
|
||||||
|
aria-label="Selecionar todos"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</th>
|
||||||
<th className="py-3 pr-4 font-medium">Identificação</th>
|
<th className="py-3 pr-4 font-medium">Identificação</th>
|
||||||
<th className="py-3 pr-4 font-medium">E-mail técnico</th>
|
<th className="py-3 pr-4 font-medium">E-mail técnico</th>
|
||||||
<th className="py-3 pr-4 font-medium">Perfil</th>
|
<th className="py-3 pr-4 font-medium">Perfil</th>
|
||||||
|
|
@ -955,6 +1350,22 @@ async function handleDeleteUser() {
|
||||||
const machineId = extractMachineId(user.email)
|
const machineId = extractMachineId(user.email)
|
||||||
return (
|
return (
|
||||||
<tr key={user.id} className="hover:bg-slate-50">
|
<tr key={user.id} className="hover:bg-slate-50">
|
||||||
|
<td className="py-3 pr-2">
|
||||||
|
<div className="flex items-center justify-center">
|
||||||
|
<Checkbox
|
||||||
|
checked={machineSelection.has(user.id)}
|
||||||
|
onCheckedChange={(checked) => {
|
||||||
|
setMachineSelection((prev) => {
|
||||||
|
const next = new Set(prev)
|
||||||
|
if (checked) next.add(user.id)
|
||||||
|
else next.delete(user.id)
|
||||||
|
return next
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
aria-label="Selecionar linha"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
<td className="py-3 pr-4 font-medium text-neutral-800">{user.name || "Máquina"}</td>
|
<td className="py-3 pr-4 font-medium text-neutral-800">{user.name || "Máquina"}</td>
|
||||||
<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">
|
<td className="py-3 pr-4 text-neutral-600">
|
||||||
|
|
@ -990,7 +1401,7 @@ async function handleDeleteUser() {
|
||||||
})}
|
})}
|
||||||
{filteredMachineUsers.length === 0 ? (
|
{filteredMachineUsers.length === 0 ? (
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={5} className="py-6 text-center text-neutral-500">
|
<td colSpan={6} className="py-6 text-center text-neutral-500">
|
||||||
{machineUsers.length === 0
|
{machineUsers.length === 0
|
||||||
? "Nenhuma máquina provisionada ainda."
|
? "Nenhuma máquina provisionada ainda."
|
||||||
: "Nenhum agente encontrado para a busca atual."}
|
: "Nenhum agente encontrado para a busca atual."}
|
||||||
|
|
@ -1106,6 +1517,15 @@ async function handleDeleteUser() {
|
||||||
<table className="min-w-full table-fixed divide-y divide-slate-200 text-sm">
|
<table className="min-w-full table-fixed divide-y divide-slate-200 text-sm">
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="text-left text-xs uppercase tracking-wide text-neutral-500">
|
<tr className="text-left text-xs uppercase tracking-wide text-neutral-500">
|
||||||
|
<th className="w-10 py-3 pr-2 font-medium">
|
||||||
|
<div className="flex items-center justify-center">
|
||||||
|
<Checkbox
|
||||||
|
checked={allInvitesSelected || (someInvitesSelected && "indeterminate")}
|
||||||
|
onCheckedChange={(value) => toggleInvitesSelectAll(!!value)}
|
||||||
|
aria-label="Selecionar todos"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</th>
|
||||||
<th className="py-3 pr-4 font-medium">Colaborador</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">Papel</th>
|
||||||
<th className="py-3 pr-4 font-medium">Espaço</th>
|
<th className="py-3 pr-4 font-medium">Espaço</th>
|
||||||
|
|
@ -1117,6 +1537,22 @@ async function handleDeleteUser() {
|
||||||
<tbody className="divide-y divide-slate-100">
|
<tbody className="divide-y divide-slate-100">
|
||||||
{invites.map((invite) => (
|
{invites.map((invite) => (
|
||||||
<tr key={invite.id} className="hover:bg-slate-50">
|
<tr key={invite.id} className="hover:bg-slate-50">
|
||||||
|
<td className="py-3 pr-2">
|
||||||
|
<div className="flex items-center justify-center">
|
||||||
|
<Checkbox
|
||||||
|
checked={inviteSelection.has(invite.id)}
|
||||||
|
onCheckedChange={(checked) => {
|
||||||
|
setInviteSelection((prev) => {
|
||||||
|
const next = new Set(prev)
|
||||||
|
if (checked) next.add(invite.id)
|
||||||
|
else next.delete(invite.id)
|
||||||
|
return next
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
aria-label="Selecionar linha"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
<td className="py-3 pr-4">
|
<td className="py-3 pr-4">
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<span className="font-medium text-neutral-800">{invite.name || invite.email}</span>
|
<span className="font-medium text-neutral-800">{invite.name || invite.email}</span>
|
||||||
|
|
@ -1167,13 +1603,24 @@ async function handleDeleteUser() {
|
||||||
))}
|
))}
|
||||||
{invites.length === 0 ? (
|
{invites.length === 0 ? (
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={6} className="py-6 text-center text-neutral-500">
|
<td colSpan={7} className="py-6 text-center text-neutral-500">
|
||||||
Nenhum convite emitido até o momento.
|
Nenhum convite emitido até o momento.
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
) : null}
|
) : null}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
<div className="mt-3 flex items-center justify-end">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="gap-2 border-rose-200 text-rose-600 hover:bg-rose-50 hover:text-rose-700 disabled:text-rose-300 disabled:border-rose-100"
|
||||||
|
disabled={selectedInvites.length === 0 || isBulkRevokingInvites}
|
||||||
|
onClick={() => setBulkRevokeInvitesOpen(true)}
|
||||||
|
>
|
||||||
|
<IconTrash className="size-4" /> Revogar selecionados
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
@ -1508,6 +1955,184 @@ async function handleDeleteUser() {
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
{/* Bulk actions */}
|
||||||
|
<Dialog open={bulkDeleteTeamOpen} onOpenChange={setBulkDeleteTeamOpen}>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Remover membros selecionados</DialogTitle>
|
||||||
|
<DialogDescription>Revoga o acesso imediatamente. Registros históricos permanecem.</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="max-h-64 space-y-2 overflow-auto">
|
||||||
|
{selectedTeamUsers.slice(0, 5).map((u) => (
|
||||||
|
<div key={`team-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>
|
||||||
|
))}
|
||||||
|
{selectedTeamUsers.length > 5 ? (
|
||||||
|
<div className="px-3 text-xs text-neutral-500">+ {selectedTeamUsers.length - 5} outros</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" onClick={() => setBulkDeleteTeamOpen(false)} disabled={isBulkDeletingTeam}>
|
||||||
|
Cancelar
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
disabled={isBulkDeletingTeam}
|
||||||
|
onClick={async () => {
|
||||||
|
setIsBulkDeletingTeam(true)
|
||||||
|
try {
|
||||||
|
await performBulkDeleteUsers(selectedTeamUsers.map((u) => u.id))
|
||||||
|
setTeamSelection(new Set())
|
||||||
|
setBulkDeleteTeamOpen(false)
|
||||||
|
toast.success("Remoção concluída")
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : "Falha ao excluir selecionados"
|
||||||
|
toast.error(message)
|
||||||
|
} finally {
|
||||||
|
setIsBulkDeletingTeam(false)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Excluir selecionados
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</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>
|
||||||
|
|
||||||
|
<Dialog open={bulkRevokeInvitesOpen} onOpenChange={setBulkRevokeInvitesOpen}>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Revogar convites selecionados</DialogTitle>
|
||||||
|
<DialogDescription>Os links serão invalidados imediatamente.</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="max-h-64 space-y-2 overflow-auto">
|
||||||
|
{selectedInvites.slice(0, 5).map((i) => (
|
||||||
|
<div key={`invite-revoke-${i.id}`} className="rounded-md bg-slate-100 px-3 py-2 text-sm">
|
||||||
|
{i.email} <span className="text-neutral-500">— {formatRole(i.role)}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{selectedInvites.length > 5 ? (
|
||||||
|
<div className="px-3 text-xs text-neutral-500">+ {selectedInvites.length - 5} outros</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" onClick={() => setBulkRevokeInvitesOpen(false)} disabled={isBulkRevokingInvites}>
|
||||||
|
Cancelar
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
disabled={isBulkRevokingInvites}
|
||||||
|
onClick={async () => {
|
||||||
|
setIsBulkRevokingInvites(true)
|
||||||
|
try {
|
||||||
|
const ids = selectedInvites.filter((i) => i.status === "pending").map((i) => i.id)
|
||||||
|
await performBulkRevokeInvites(ids)
|
||||||
|
setInviteSelection(new Set())
|
||||||
|
setBulkRevokeInvitesOpen(false)
|
||||||
|
toast.success("Convites revogados")
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : "Falha ao revogar convites"
|
||||||
|
toast.error(message)
|
||||||
|
} finally {
|
||||||
|
setIsBulkRevokingInvites(false)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Revogar selecionados
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue