Refine admin users filters with searchable combobox

This commit is contained in:
Esdras Renan 2025-10-24 10:15:30 -03:00
parent b51d0770d3
commit e47ea5eecc

View file

@ -60,6 +60,7 @@ import {
import { Textarea } from "@/components/ui/textarea" import { Textarea } from "@/components/ui/textarea"
import { Badge } from "@/components/ui/badge" import { Badge } from "@/components/ui/badge"
import { Avatar, AvatarFallback } from "@/components/ui/avatar" import { Avatar, AvatarFallback } from "@/components/ui/avatar"
import { SearchableCombobox, type SearchableComboboxOption } from "@/components/ui/searchable-combobox"
export type AdminAccount = { export type AdminAccount = {
id: string id: string
@ -196,6 +197,32 @@ function AccountsTable({ initialAccounts }: { initialAccounts: AdminAccount[] })
return Array.from(map.entries()).map(([id, name]) => ({ id, name })) return Array.from(map.entries()).map(([id, name]) => ({ id, name }))
}, [accounts]) }, [accounts])
const roleSelectOptions = useMemo<SearchableComboboxOption[]>(
() => ROLE_OPTIONS_DISPLAY.map((option) => ({ value: option.value, label: option.label })),
[],
)
const roleFilterOptions = useMemo<SearchableComboboxOption[]>(
() => [{ value: "all", label: "Todos os papéis" }, ...roleSelectOptions],
[roleSelectOptions],
)
const companyFilterOptions = useMemo<SearchableComboboxOption[]>(
() => [
{ value: "all", label: "Todas as empresas" },
...companies.map((company) => ({ value: company.id, label: company.name })),
],
[companies],
)
const editCompanyOptions = useMemo<SearchableComboboxOption[]>(
() => [
{ value: NO_COMPANY_SELECT_VALUE, label: "Sem empresa vinculada" },
...companies.map((company) => ({ value: company.id, label: company.name })),
],
[companies],
)
const openDeleteDialog = useCallback((ids: string[]) => { const openDeleteDialog = useCallback((ids: string[]) => {
setDeleteDialogIds(ids) setDeleteDialogIds(ids)
}, []) }, [])
@ -381,30 +408,27 @@ function AccountsTable({ initialAccounts }: { initialAccounts: AdminAccount[] })
<div className="flex flex-wrap gap-3"> <div className="flex flex-wrap gap-3">
<div className="flex items-center gap-2 text-muted-foreground"> <div className="flex items-center gap-2 text-muted-foreground">
<IconFilter className="size-4" /> <IconFilter className="size-4" />
<Select value={roleFilter} onValueChange={(value: typeof roleFilter) => setRoleFilter(value)}> <SearchableCombobox
<SelectTrigger className="h-9 w-[12rem]"> value={roleFilter}
<SelectValue placeholder="Papel" /> onValueChange={(next) => {
</SelectTrigger> const normalized =
<SelectContent> next === "MANAGER" || next === "COLLABORATOR" ? next : "all"
<SelectItem value="all">Todos os papéis</SelectItem> setRoleFilter(normalized)
<SelectItem value="MANAGER">Gestores</SelectItem> }}
<SelectItem value="COLLABORATOR">Colaboradores</SelectItem> options={roleFilterOptions}
</SelectContent> placeholder="Todos os papéis"
</Select> searchPlaceholder="Buscar papel..."
className="md:w-[12rem]"
/>
</div> </div>
<Select value={companyFilter} onValueChange={setCompanyFilter}> <SearchableCombobox
<SelectTrigger className="h-9 w-[16rem]"> value={companyFilter}
<SelectValue placeholder="Empresa" /> onValueChange={(next) => setCompanyFilter(next ?? "all")}
</SelectTrigger> options={companyFilterOptions}
<SelectContent> placeholder="Todas as empresas"
<SelectItem value="all">Todas as empresas</SelectItem> searchPlaceholder="Buscar empresa..."
{companies.map((company) => ( className="md:w-[16rem]"
<SelectItem key={company.id} value={company.id}> />
{company.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div> </div>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
@ -424,18 +448,18 @@ function AccountsTable({ initialAccounts }: { initialAccounts: AdminAccount[] })
<Table className="w-full table-fixed text-sm"> <Table className="w-full table-fixed text-sm">
<TableHeader className="bg-muted"> <TableHeader className="bg-muted">
<TableRow> <TableRow>
<TableHead>Usuário</TableHead> <TableHead className="px-6">Usuário</TableHead>
<TableHead>Empresa</TableHead> <TableHead className="px-4">Empresa</TableHead>
<TableHead>Papel</TableHead> <TableHead className="px-4">Papel</TableHead>
<TableHead>Último acesso</TableHead> <TableHead className="px-4">Último acesso</TableHead>
<TableHead className="text-right">Ações</TableHead> <TableHead className="px-4 text-right">Ações</TableHead>
<TableHead className="text-right">Selecionar</TableHead> <TableHead className="px-4 text-right">Selecionar</TableHead>
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{filteredAccounts.length === 0 ? ( {filteredAccounts.length === 0 ? (
<TableRow> <TableRow>
<TableCell colSpan={5} className="text-center text-sm text-muted-foreground"> <TableCell colSpan={6} className="px-6 py-6 text-center text-sm text-muted-foreground">
Nenhum usuário encontrado. Nenhum usuário encontrado.
</TableCell> </TableCell>
</TableRow> </TableRow>
@ -449,7 +473,7 @@ function AccountsTable({ initialAccounts }: { initialAccounts: AdminAccount[] })
.join("") .join("")
return ( return (
<TableRow key={account.id} className="hover:bg-muted/40"> <TableRow key={account.id} className="hover:bg-muted/40">
<TableCell> <TableCell className="px-6 py-3">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Avatar className="size-9 border border-border/60"> <Avatar className="size-9 border border-border/60">
<AvatarFallback>{initials || account.email.charAt(0).toUpperCase()}</AvatarFallback> <AvatarFallback>{initials || account.email.charAt(0).toUpperCase()}</AvatarFallback>
@ -460,14 +484,16 @@ function AccountsTable({ initialAccounts }: { initialAccounts: AdminAccount[] })
</div> </div>
</div> </div>
</TableCell> </TableCell>
<TableCell className="text-sm text-muted-foreground"> <TableCell className="px-4 py-3 text-sm text-muted-foreground">
{account.companyName ?? <span className="italic text-muted-foreground/70">Sem empresa</span>} {account.companyName ?? <span className="italic text-muted-foreground/70">Sem empresa</span>}
</TableCell> </TableCell>
<TableCell className="text-sm"> <TableCell className="px-4 py-3 text-sm">
<Badge variant="secondary">{ROLE_LABEL[account.role]}</Badge> <Badge variant="secondary">{ROLE_LABEL[account.role]}</Badge>
</TableCell> </TableCell>
<TableCell className="text-xs text-muted-foreground">{formatDate(account.lastSeenAt)}</TableCell> <TableCell className="px-4 py-3 text-xs text-muted-foreground">
<TableCell className="text-right"> {formatDate(account.lastSeenAt)}
</TableCell>
<TableCell className="px-4 py-3 text-right align-middle">
<div className="flex justify-end gap-2"> <div className="flex justify-end gap-2">
<Button <Button
variant="outline" variant="outline"
@ -490,13 +516,16 @@ function AccountsTable({ initialAccounts }: { initialAccounts: AdminAccount[] })
</Button> </Button>
</div> </div>
</TableCell> </TableCell>
<TableCell className="text-right"> <TableCell className="px-4 py-3 text-right align-middle">
<div className="flex justify-end">
<Checkbox <Checkbox
checked={rowSelection[account.id] ?? false} checked={rowSelection[account.id] ?? false}
onCheckedChange={(checked) => onCheckedChange={(checked) =>
setRowSelection((prev) => ({ ...prev, [account.id]: Boolean(checked) })) setRowSelection((prev) => ({ ...prev, [account.id]: Boolean(checked) }))
} }
aria-label={`Selecionar ${account.name}`}
/> />
</div>
</TableCell> </TableCell>
</TableRow> </TableRow>
) )
@ -571,50 +600,36 @@ function AccountsTable({ initialAccounts }: { initialAccounts: AdminAccount[] })
/> />
</div> </div>
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor="edit-role">Papel</Label> <Label>Papel</Label>
<Select <SearchableCombobox
value={editForm.role} value={editForm.role}
onValueChange={(value) => onValueChange={(next) =>
setEditForm((prev) => ({ ...prev, role: value as AdminAccount["role"] }))
}
disabled={isSavingAccount}
>
<SelectTrigger id="edit-role">
<SelectValue placeholder="Selecione" />
</SelectTrigger>
<SelectContent>
{ROLE_OPTIONS_DISPLAY.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="grid gap-2">
<Label htmlFor="edit-company">Empresa vinculada</Label>
<Select
value={editForm.companyId || NO_COMPANY_SELECT_VALUE}
onValueChange={(value) =>
setEditForm((prev) => ({ setEditForm((prev) => ({
...prev, ...prev,
companyId: value === NO_COMPANY_SELECT_VALUE ? "" : value, role: next === "MANAGER" || next === "COLLABORATOR" ? next : prev.role,
})) }))
} }
options={roleSelectOptions}
placeholder="Selecione"
searchPlaceholder="Buscar papel..."
disabled={isSavingAccount} disabled={isSavingAccount}
> />
<SelectTrigger id="edit-company"> </div>
<SelectValue placeholder="Sem empresa vinculada" /> <div className="grid gap-2">
</SelectTrigger> <Label>Empresa vinculada</Label>
<SelectContent> <SearchableCombobox
<SelectItem value={NO_COMPANY_SELECT_VALUE}>Sem empresa vinculada</SelectItem> value={editForm.companyId || NO_COMPANY_SELECT_VALUE}
{companies.map((company) => ( onValueChange={(next) =>
<SelectItem key={company.id} value={company.id}> setEditForm((prev) => ({
{company.name} ...prev,
</SelectItem> companyId: next === NO_COMPANY_SELECT_VALUE || next === null ? "" : next,
))} }))
</SelectContent> }
</Select> options={editCompanyOptions}
placeholder="Sem empresa vinculada"
searchPlaceholder="Buscar empresa..."
disabled={isSavingAccount}
/>
</div> </div>
</div> </div>