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