Some checks failed
Build & Release / build (push) Failing after 33s
Each destination route's search must match its validateSearch shape exactly:
- Detail pages (tab-based): { tab: '...' }
- List pages with extra filters: include status, instructorId, view, categoryId etc.
- Form pages (enrollments/new, repairs/new): include only their specific fields
- use-pagination.ts: fix search reducer to use (prev: any) instead of invalid cast
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
142 lines
4.8 KiB
TypeScript
142 lines
4.8 KiB
TypeScript
import { createFileRoute, useNavigate } from '@tanstack/react-router'
|
|
import { useQuery } from '@tanstack/react-query'
|
|
import { useState } from 'react'
|
|
import { globalMemberListOptions, type MemberWithAccount } from '@/api/members'
|
|
import { usePagination } from '@/hooks/use-pagination'
|
|
import { DataTable, type Column } from '@/components/shared/data-table'
|
|
import { Badge } from '@/components/ui/badge'
|
|
import { Input } from '@/components/ui/input'
|
|
import { Button } from '@/components/ui/button'
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuTrigger,
|
|
} from '@/components/ui/dropdown-menu'
|
|
import { Search, Plus, MoreVertical, Pencil, Users } from 'lucide-react'
|
|
import { useAuthStore } from '@/stores/auth.store'
|
|
|
|
export const Route = createFileRoute('/_authenticated/members/')({
|
|
validateSearch: (search: Record<string, unknown>) => ({
|
|
page: Number(search.page) || 1,
|
|
limit: Number(search.limit) || 25,
|
|
q: (search.q as string) || undefined,
|
|
sort: (search.sort as string) || undefined,
|
|
order: (search.order as 'asc' | 'desc') || 'asc',
|
|
}),
|
|
component: MembersListPage,
|
|
})
|
|
|
|
function MembersListPage() {
|
|
const navigate = useNavigate()
|
|
const hasPermission = useAuthStore((s) => s.hasPermission)
|
|
const { params, setPage, setSearch, setSort } = usePagination()
|
|
const [searchInput, setSearchInput] = useState(params.q ?? '')
|
|
|
|
const { data, isLoading } = useQuery(globalMemberListOptions(params))
|
|
|
|
function handleSearchSubmit(e: React.FormEvent) {
|
|
e.preventDefault()
|
|
setSearch(searchInput)
|
|
}
|
|
|
|
const memberColumns: Column<MemberWithAccount>[] = [
|
|
{
|
|
key: 'memberNumber',
|
|
header: '#',
|
|
render: (row) => <span className="font-mono text-sm text-muted-foreground">{row.memberNumber ?? '-'}</span>,
|
|
},
|
|
{
|
|
key: 'last_name',
|
|
header: 'Name',
|
|
sortable: true,
|
|
render: (row) => <span className="font-medium">{row.firstName} {row.lastName}</span>,
|
|
},
|
|
{
|
|
key: 'email',
|
|
header: 'Email',
|
|
sortable: true,
|
|
render: (row) => row.email ?? <span className="text-muted-foreground">-</span>,
|
|
},
|
|
{
|
|
key: 'phone',
|
|
header: 'Phone',
|
|
render: (row) => row.phone ?? <span className="text-muted-foreground">-</span>,
|
|
},
|
|
{
|
|
key: 'accountName',
|
|
header: 'Account',
|
|
render: (row) => row.accountName ?? <span className="text-muted-foreground">-</span>,
|
|
},
|
|
{
|
|
key: 'status',
|
|
header: 'Status',
|
|
render: (row) => row.isMinor ? <Badge variant="secondary">Minor</Badge> : <Badge>Adult</Badge>,
|
|
},
|
|
{
|
|
key: 'actions',
|
|
header: '',
|
|
render: (row) => (
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button variant="ghost" size="sm" className="h-8 w-8 p-0" onClick={(e) => e.stopPropagation()}>
|
|
<MoreVertical className="h-4 w-4" />
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="end">
|
|
<DropdownMenuItem onClick={() => navigate({ to: '/members/$memberId', params: { memberId: row.id }, search: { tab: 'details' } })}>
|
|
<Pencil className="mr-2 h-4 w-4" />
|
|
Edit
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem onClick={() => navigate({ to: '/accounts/$accountId', params: { accountId: row.accountId } })}>
|
|
<Users className="mr-2 h-4 w-4" />
|
|
View Account
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
),
|
|
},
|
|
]
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<div className="flex items-center justify-between">
|
|
<h1 className="text-2xl font-bold">Members</h1>
|
|
{hasPermission('accounts.edit') && (
|
|
<Button onClick={() => navigate({ to: '/accounts/new' })}>
|
|
<Plus className="mr-2 h-4 w-4" />
|
|
New Member
|
|
</Button>
|
|
)}
|
|
</div>
|
|
|
|
<form onSubmit={handleSearchSubmit} className="flex gap-2 max-w-sm">
|
|
<div className="relative flex-1">
|
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
|
<Input
|
|
placeholder="Search members..."
|
|
value={searchInput}
|
|
onChange={(e) => setSearchInput(e.target.value)}
|
|
className="pl-9"
|
|
/>
|
|
</div>
|
|
<Button type="submit" variant="secondary">Search</Button>
|
|
</form>
|
|
|
|
<DataTable
|
|
columns={memberColumns}
|
|
data={data?.data ?? []}
|
|
loading={isLoading}
|
|
page={params.page}
|
|
totalPages={data?.pagination.totalPages ?? 1}
|
|
total={data?.pagination.total ?? 0}
|
|
sort={params.sort}
|
|
order={params.order}
|
|
onPageChange={setPage}
|
|
onSort={setSort}
|
|
onRowClick={(member) => navigate({ to: '/members/$memberId', params: { memberId: member.id }, search: { tab: 'details' } })}
|
|
/>
|
|
</div>
|
|
)
|
|
}
|