Refine admin users filters with searchable combobox
This commit is contained in:
parent
b51d0770d3
commit
e47ea5eecc
1 changed files with 98 additions and 83 deletions
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue