import { useState } from 'react' import { createFileRoute, useParams, useNavigate } from '@tanstack/react-router' import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import { memberListOptions, memberMutations, memberKeys } from '@/api/members' import { identifierListOptions, identifierMutations, identifierKeys } from '@/api/identifiers' import { MemberForm } from '@/components/accounts/member-form' import { IdentifierForm } from '@/components/accounts/identifier-form' import { usePagination } from '@/hooks/use-pagination' import { Button } from '@/components/ui/button' import { Badge } from '@/components/ui/badge' import { Input } from '@/components/ui/input' import { Skeleton } from '@/components/ui/skeleton' import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog' import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu' import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table' import { Plus, Trash2, CreditCard, ChevronDown, ChevronRight, ChevronLeft, MoreVertical, Pencil, Search, ArrowUpDown, ArrowUp, ArrowDown } from 'lucide-react' import { toast } from 'sonner' export const Route = createFileRoute('/_authenticated/accounts/$accountId/members')({ validateSearch: (search: Record) => ({ 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: MembersTab, }) const ID_TYPE_LABELS: Record = { drivers_license: "Driver's License", passport: 'Passport', school_id: 'School ID', } function MemberIdentifiers({ memberId }: { memberId: string }) { const queryClient = useQueryClient() const [addOpen, setAddOpen] = useState(false) const { data, isLoading } = useQuery(identifierListOptions(memberId)) const createMutation = useMutation({ mutationFn: (data: Record) => identifierMutations.create(memberId, data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: identifierKeys.all(memberId) }) toast.success('ID added') setAddOpen(false) }, onError: (err) => toast.error(err.message), }) const deleteMutation = useMutation({ mutationFn: identifierMutations.delete, onSuccess: () => { queryClient.invalidateQueries({ queryKey: identifierKeys.all(memberId) }) toast.success('ID removed') }, onError: (err) => toast.error(err.message), }) const identifiers = data?.data ?? [] return (
Identity Documents Add Identity Document createMutation.mutate(data)} loading={createMutation.isPending} />
{isLoading ? (

Loading...

) : identifiers.length === 0 ? (

No IDs on file

) : (
{identifiers.map((id) => (
{ID_TYPE_LABELS[id.type] ?? id.type} {id.isPrimary && Primary}
{id.value} {id.issuingAuthority && ( — {id.issuingAuthority} )} {id.expiresAt && ( Exp: {id.expiresAt} )}
{(id.imageFrontFileId || id.imageBackFileId) && ( Has images )}
))}
)}
) } function SortableHeader({ label, sortKey, currentSort, currentOrder, onSort }: { label: string sortKey: string currentSort?: string currentOrder?: 'asc' | 'desc' onSort: (sort: string, order: 'asc' | 'desc') => void }) { function handleClick() { if (currentSort === sortKey) { onSort(sortKey, currentOrder === 'asc' ? 'desc' : 'asc') } else { onSort(sortKey, 'asc') } } const Icon = currentSort !== sortKey ? ArrowUpDown : currentOrder === 'asc' ? ArrowUp : ArrowDown return ( {label} ) } function MembersTab() { const { accountId } = useParams({ from: '/_authenticated/accounts/$accountId/members' }) const navigate = useNavigate() const queryClient = useQueryClient() const [dialogOpen, setDialogOpen] = useState(false) const [expandedMember, setExpandedMember] = useState(null) const { params, setPage, setSearch, setSort } = usePagination() const [searchInput, setSearchInput] = useState(params.q ?? '') const { data, isLoading } = useQuery(memberListOptions(accountId, params)) const createMutation = useMutation({ mutationFn: (data: Record) => memberMutations.create(accountId, data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: memberKeys.all(accountId) }) toast.success('Member added') setDialogOpen(false) }, onError: (err) => toast.error(err.message), }) const deleteMutation = useMutation({ mutationFn: memberMutations.delete, onSuccess: () => { queryClient.invalidateQueries({ queryKey: memberKeys.all(accountId) }) toast.success('Member removed') }, onError: (err) => toast.error(err.message), }) function handleSearchSubmit(e: React.FormEvent) { e.preventDefault() setSearch(searchInput) } const members = data?.data ?? [] const totalPages = data?.pagination.totalPages ?? 1 const total = data?.pagination.total ?? 0 if (isLoading) { return (
{Array.from({ length: 5 }).map((_, i) => ( ))}
) } return (

Members

Add Member
setSearchInput(e.target.value)} className="pl-9" />
# Phone Status {members.length === 0 ? ( No members found ) : ( members.map((m) => ( <> {m.memberNumber ?? '-'} {m.firstName} {m.lastName} {m.email ?? '-'} {m.phone ?? '-'} {m.isMinor ? Minor : Adult} navigate({ to: '/members/$memberId', params: { memberId: m.id }, search: {} as any, })}> Edit setExpandedMember(expandedMember === m.id ? null : m.id)}> {expandedMember === m.id ? 'Hide IDs' : 'View IDs'} deleteMutation.mutate(m.id)}> Delete {expandedMember === m.id && ( )} )) )}
{total} total
Page {params.page} of {totalPages}
) }