Add confirmation dialog for client deletion and align machine badges

This commit is contained in:
Esdras Renan 2025-10-18 19:52:05 -03:00
parent 2400f34c80
commit 1c7309a2b6
2 changed files with 182 additions and 112 deletions

View file

@ -25,6 +25,14 @@ import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { Checkbox } from "@/components/ui/checkbox" import { Checkbox } from "@/components/ui/checkbox"
import { Input } from "@/components/ui/input" import { Input } from "@/components/ui/input"
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
import { import {
Select, Select,
SelectContent, SelectContent,
@ -79,6 +87,7 @@ export function AdminClientsManager({ initialClients }: { initialClients: AdminC
const [rowSelection, setRowSelection] = useState({}) const [rowSelection, setRowSelection] = useState({})
const [sorting, setSorting] = useState<SortingState>([{ id: "name", desc: false }]) const [sorting, setSorting] = useState<SortingState>([{ id: "name", desc: false }])
const [isPending, startTransition] = useTransition() const [isPending, startTransition] = useTransition()
const [deleteDialogIds, setDeleteDialogIds] = useState<string[]>([])
const companies = useMemo(() => { const companies = useMemo(() => {
const entries = new Map<string, string>() const entries = new Map<string, string>()
@ -104,6 +113,11 @@ export function AdminClientsManager({ initialClients }: { initialClients: AdminC
}) })
}, [clients, roleFilter, companyFilter, search]) }, [clients, roleFilter, companyFilter, search])
const deleteTargets = useMemo(
() => clients.filter((client) => deleteDialogIds.includes(client.id)),
[clients, deleteDialogIds],
)
const handleDelete = useCallback( const handleDelete = useCallback(
(ids: string[]) => { (ids: string[]) => {
if (ids.length === 0) return if (ids.length === 0) return
@ -124,6 +138,7 @@ export function AdminClientsManager({ initialClients }: { initialClients: AdminC
if (deletedIds.length > 0) { if (deletedIds.length > 0) {
setClients((prev) => prev.filter((client) => !deletedIds.includes(client.id))) setClients((prev) => prev.filter((client) => !deletedIds.includes(client.id)))
setRowSelection({}) setRowSelection({})
setDeleteDialogIds([])
} }
toast.success( toast.success(
deletedIds.length === 1 deletedIds.length === 1
@ -239,14 +254,14 @@ export function AdminClientsManager({ initialClients }: { initialClients: AdminC
size="sm" size="sm"
className="border-rose-200 text-rose-600 hover:bg-rose-50 hover:text-rose-700" className="border-rose-200 text-rose-600 hover:bg-rose-50 hover:text-rose-700"
disabled={isPending} disabled={isPending}
onClick={() => handleDelete([row.original.id])} onClick={() => setDeleteDialogIds([row.original.id])}
> >
<IconTrash className="mr-2 size-4" /> Remover <IconTrash className="mr-2 size-4" /> Remover
</Button> </Button>
), ),
}, },
], ],
[handleDelete, isPending] [isPending, setDeleteDialogIds]
) )
const table = useReactTable({ const table = useReactTable({
@ -268,124 +283,179 @@ export function AdminClientsManager({ initialClients }: { initialClients: AdminC
}) })
const selectedRows = table.getSelectedRowModel().flatRows.map((row) => row.original) const selectedRows = table.getSelectedRowModel().flatRows.map((row) => row.original)
const isBulkDelete = deleteTargets.length > 1
const dialogTitle = isBulkDelete ? "Remover clientes selecionados" : "Remover cliente"
const dialogDescription = isBulkDelete
? "Essa ação remove os clientes selecionados e revoga o acesso ao portal."
: "Essa ação remove o cliente escolhido e revoga o acesso ao portal."
const previewTargets = deleteTargets.slice(0, 3)
const remainingCount = deleteTargets.length - previewTargets.length
return ( return (
<div className="space-y-4"> <>
<div className="flex flex-col gap-3 rounded-xl border border-slate-200 bg-white p-4 shadow-sm md:flex-row md:items-center md:justify-between"> <div className="space-y-4">
<div className="flex items-center gap-2 text-sm text-neutral-600"> <div className="flex flex-col gap-3 rounded-xl border border-slate-200 bg-white p-4 shadow-sm md:flex-row md:items-center md:justify-between">
<IconUser className="size-4" /> <div className="flex items-center gap-2 text-sm text-neutral-600">
{clients.length} cliente{clients.length === 1 ? "" : "s"} <IconUser className="size-4" />
</div> {clients.length} cliente{clients.length === 1 ? "" : "s"}
<div className="flex flex-col gap-2 md:flex-row md:items-center">
<div className="flex items-center gap-2">
<Input
placeholder="Buscar por nome, e-mail ou empresa"
value={search}
onChange={(event) => setSearch(event.target.value)}
className="h-9 w-full md:w-72"
/>
<Button variant="outline" size="icon" className="md:hidden">
<IconFilter className="size-4" />
</Button>
</div> </div>
<div className="flex flex-wrap items-center gap-2"> <div className="flex flex-col gap-2 md:flex-row md:items-center">
<Select value={roleFilter} onValueChange={(value) => setRoleFilter(value as typeof roleFilter)}> <div className="flex items-center gap-2">
<SelectTrigger className="h-9 w-40"> <Input
<SelectValue placeholder="Perfil" /> placeholder="Buscar por nome, e-mail ou empresa"
</SelectTrigger> value={search}
<SelectContent> onChange={(event) => setSearch(event.target.value)}
<SelectItem value="all">Todos os perfis</SelectItem> className="h-9 w-full md:w-72"
<SelectItem value="MANAGER">Gestores</SelectItem> />
<SelectItem value="COLLABORATOR">Colaboradores</SelectItem> <Button variant="outline" size="icon" className="md:hidden">
</SelectContent> <IconFilter className="size-4" />
</Select> </Button>
<Select value={companyFilter} onValueChange={(value) => setCompanyFilter(value)}> </div>
<SelectTrigger className="h-9 w-48"> <div className="flex flex-wrap items-center gap-2">
<SelectValue placeholder="Empresa" /> <Select value={roleFilter} onValueChange={(value) => setRoleFilter(value as typeof roleFilter)}>
</SelectTrigger> <SelectTrigger className="h-9 w-40">
<SelectContent className="max-h-64"> <SelectValue placeholder="Perfil" />
<SelectItem value="all">Todas as empresas</SelectItem> </SelectTrigger>
{companies.map((company) => ( <SelectContent>
<SelectItem key={company.id} value={company.id}> <SelectItem value="all">Todos os perfis</SelectItem>
{company.name} <SelectItem value="MANAGER">Gestores</SelectItem>
</SelectItem> <SelectItem value="COLLABORATOR">Colaboradores</SelectItem>
))} </SelectContent>
</SelectContent> </Select>
</Select> <Select value={companyFilter} onValueChange={(value) => setCompanyFilter(value)}>
<Button <SelectTrigger className="h-9 w-48">
variant="outline" <SelectValue placeholder="Empresa" />
size="sm" </SelectTrigger>
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" <SelectContent className="max-h-64">
disabled={selectedRows.length === 0 || isPending} <SelectItem value="all">Todas as empresas</SelectItem>
onClick={() => handleDelete(selectedRows.map((row) => row.id))} {companies.map((company) => (
> <SelectItem key={company.id} value={company.id}>
<IconTrash className="size-4" /> {company.name}
Excluir selecionados </SelectItem>
</Button> ))}
</SelectContent>
</Select>
<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={selectedRows.length === 0 || isPending}
onClick={() => setDeleteDialogIds(selectedRows.map((row) => row.id))}
>
<IconTrash className="size-4" />
Excluir selecionados
</Button>
</div>
</div> </div>
</div> </div>
</div>
<div className="overflow-hidden rounded-xl border border-slate-200 bg-white shadow-sm"> <div className="overflow-hidden rounded-xl border border-slate-200 bg-white shadow-sm">
<Table> <Table>
<TableHeader className="bg-slate-50"> <TableHeader className="bg-slate-50">
{table.getHeaderGroups().map((headerGroup) => ( {table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id} className="border-slate-200"> <TableRow key={headerGroup.id} className="border-slate-200">
{headerGroup.headers.map((header) => ( {headerGroup.headers.map((header) => (
<TableHead key={header.id} className="text-xs uppercase tracking-wide text-neutral-500"> <TableHead key={header.id} className="text-xs uppercase tracking-wide text-neutral-500">
{header.isPlaceholder ? null : header.column.columnDef.header instanceof Function {header.isPlaceholder ? null : header.column.columnDef.header instanceof Function
? header.column.columnDef.header(header.getContext()) ? header.column.columnDef.header(header.getContext())
: header.column.columnDef.header} : header.column.columnDef.header}
</TableHead> </TableHead>
))}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows.length === 0 ? (
<TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center text-sm text-neutral-500">
Nenhum cliente encontrado para os filtros selecionados.
</TableCell>
</TableRow>
) : (
table.getRowModel().rows.map((row) => (
<TableRow key={row.id} data-state={row.getIsSelected() && "selected"}>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id} className="align-middle text-sm text-neutral-700">
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))} ))}
</TableRow> </TableRow>
)) ))}
)} </TableHeader>
</TableBody> <TableBody>
</Table> {table.getRowModel().rows.length === 0 ? (
<TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center text-sm text-neutral-500">
Nenhum cliente encontrado para os filtros selecionados.
</TableCell>
</TableRow>
) : (
table.getRowModel().rows.map((row) => (
<TableRow key={row.id} data-state={row.getIsSelected() && "selected"}>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id} className="align-middle text-sm text-neutral-700">
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
))
)}
</TableBody>
</Table>
</div>
<div className="flex flex-col items-center justify-between gap-3 text-sm text-neutral-600 md:flex-row">
<div>
Página {table.getState().pagination.pageIndex + 1} de {table.getPageCount() || 1}
</div>
<div className="flex items-center gap-2">
<Button
variant="outline"
size="icon"
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
<IconChevronLeft className="size-4" />
</Button>
<Button
variant="outline"
size="icon"
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
<IconChevronRight className="size-4" />
</Button>
</div>
</div>
</div> </div>
<div className="flex flex-col items-center justify-between gap-3 text-sm text-neutral-600 md:flex-row"> <Dialog
<div> open={deleteDialogIds.length > 0}
Página {table.getState().pagination.pageIndex + 1} de {table.getPageCount() || 1} onOpenChange={(open) => {
</div> if (!open && !isPending) {
<div className="flex items-center gap-2"> setDeleteDialogIds([])
<Button }
variant="outline" }}
size="icon" >
onClick={() => table.previousPage()} <DialogContent>
disabled={!table.getCanPreviousPage()} <DialogHeader>
> <DialogTitle>{dialogTitle}</DialogTitle>
<IconChevronLeft className="size-4" /> <DialogDescription>{dialogDescription}</DialogDescription>
</Button> </DialogHeader>
<Button {deleteTargets.length > 0 ? (
variant="outline" <div className="space-y-3 py-2 text-sm text-neutral-600">
size="icon" <p>
onClick={() => table.nextPage()} Confirme a exclusão de {isBulkDelete ? `${deleteTargets.length} clientes selecionados` : "um cliente"}. O acesso ao portal será revogado imediatamente.
disabled={!table.getCanNextPage()} </p>
> <ul className="space-y-1">
<IconChevronRight className="size-4" /> {previewTargets.map((target) => (
</Button> <li key={target.id} className="rounded-md bg-slate-100 px-3 py-2 text-sm text-neutral-800">
</div> <span className="font-medium">{target.name}</span>
</div> <span className="text-neutral-500"> {target.email}</span>
</div> </li>
))}
{remainingCount > 0 ? (
<li className="px-3 py-1 text-xs text-neutral-500">+ {remainingCount} outro{remainingCount === 1 ? "" : "s"}</li>
) : null}
</ul>
</div>
) : null}
<DialogFooter>
<Button variant="outline" onClick={() => setDeleteDialogIds([])} disabled={isPending}>
Cancelar
</Button>
<Button
variant="destructive"
onClick={() => handleDelete(deleteDialogIds)}
disabled={isPending}
>
{isPending ? "Removendo..." : isBulkDelete ? "Excluir clientes" : "Excluir cliente"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</>
) )
} }

View file

@ -1379,7 +1379,7 @@ export function MachineDetails({ machine }: MachineDetailsProps) {
{isActive ? (togglingActive ? "Desativando..." : "Desativar") : togglingActive ? "Reativando..." : "Reativar"} {isActive ? (togglingActive ? "Desativando..." : "Desativar") : togglingActive ? "Reativando..." : "Reativar"}
</Button> </Button>
{machine.registeredBy ? ( {machine.registeredBy ? (
<Badge variant="outline" className="inline-flex h-9 items-center rounded-full border-slate-300 bg-slate-100 px-3 text-sm font-medium text-slate-700"> <Badge className="inline-flex h-9 items-center gap-2 rounded-full border border-slate-200 bg-white px-3 text-sm font-semibold text-neutral-700">
Registrada via {machine.registeredBy} Registrada via {machine.registeredBy}
</Badge> </Badge>
) : null} ) : null}