import { useState } from 'react' import { createFileRoute, useParams, useNavigate, Link } from '@tanstack/react-router' import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import { api } from '@/lib/api-client' import { queryOptions } from '@tanstack/react-query' import { identifierListOptions, identifierMutations, identifierKeys } from '@/api/identifiers' import { enrollmentListOptions } from '@/api/lessons' import { moduleListOptions } from '@/api/modules' import { MemberForm } from '@/components/accounts/member-form' import { IdentifierForm, type IdentifierFiles } from '@/components/accounts/identifier-form' import { DataTable, type Column } from '@/components/shared/data-table' import { Button } from '@/components/ui/button' import { Badge } from '@/components/ui/badge' import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog' import { Skeleton } from '@/components/ui/skeleton' import { ArrowLeft, Plus, Trash2, CreditCard } from 'lucide-react' import { toast } from 'sonner' import { AvatarUpload } from '@/components/shared/avatar-upload' import { useAuthStore } from '@/stores/auth.store' import { cn } from '@/lib/utils' import type { Member } from '@/types/account' import type { Enrollment } from '@/types/lesson' function memberDetailOptions(id: string) { return queryOptions({ queryKey: ['members', 'detail', id], queryFn: () => api.get(`/v1/members/${id}`), }) } export const Route = createFileRoute('/_authenticated/members/$memberId')({ validateSearch: (search: Record) => ({ tab: (search.tab as string) || 'details', }), component: MemberDetailPage, }) // ─── Identifier images ──────────────────────────────────────────────────────── function IdentifierImages({ identifierId }: { identifierId: string }) { const { data } = useQuery({ queryKey: ['files', 'member_identifier', identifierId], queryFn: () => api.get<{ data: { id: string; path: string; category: string }[] }>('/v1/files', { entityType: 'member_identifier', entityId: identifierId, }), }) const files = data?.data ?? [] const frontFile = files.find((f) => f.category === 'front') const backFile = files.find((f) => f.category === 'back') if (!frontFile && !backFile) return null return (
{frontFile && Front} {backFile && Back}
) } const ID_TYPE_LABELS: Record = { drivers_license: "Driver's License", passport: 'Passport', school_id: 'School ID', } function statusBadge(status: string) { const variants: Record = { active: 'default', paused: 'secondary', cancelled: 'destructive', completed: 'outline', } return {status} } const enrollmentColumns: Column[] = [ { key: 'status', header: 'Status', sortable: true, render: (e) => statusBadge(e.status) }, { key: 'instructor_name', header: 'Instructor', render: (e) => <>{(e as any).instructorName ?? e.instructorId} }, { key: 'slot_info', header: 'Day / Time', render: (e) => <>{(e as any).slotInfo ?? '—'} }, { key: 'lesson_type', header: 'Lesson', render: (e) => <>{(e as any).lessonTypeName ?? '—'} }, { key: 'start_date', header: 'Start', sortable: true, render: (e) => <>{new Date(e.startDate + 'T00:00:00').toLocaleDateString()} }, { key: 'rate', header: 'Rate', render: (e) => <>{e.rate ? `$${e.rate} / ${e.billingInterval} ${e.billingUnit}` : } }, ] // ─── Page ───────────────────────────────────────────────────────────────────── function MemberDetailPage() { const { memberId } = useParams({ from: '/_authenticated/members/$memberId' }) const search = Route.useSearch() const navigate = useNavigate() const queryClient = useQueryClient() const [addIdOpen, setAddIdOpen] = useState(false) const [createLoading, setCreateLoading] = useState(false) const tab = search.tab ?? 'details' const token = useAuthStore((s) => s.token) const hasPermission = useAuthStore((s) => s.hasPermission) const { data: member, isLoading } = useQuery(memberDetailOptions(memberId)) const { data: idsData } = useQuery({ ...identifierListOptions(memberId), enabled: tab === 'identity' }) const { data: modulesData } = useQuery(moduleListOptions()) const lessonsEnabled = (modulesData?.data ?? []).some((m) => m.slug === 'lessons' && m.enabled && m.licensed) const { data: enrollmentsData } = useQuery({ ...enrollmentListOptions({ memberId, page: 1, limit: 100, order: 'asc' }), enabled: tab === 'enrollments' && lessonsEnabled, }) const updateMutation = useMutation({ mutationFn: (data: Record) => api.patch(`/v1/members/${memberId}`, data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['members', 'detail', memberId] }) toast.success('Member updated') }, onError: (err) => toast.error(err instanceof Error ? err.message : 'Update failed'), }) async function uploadIdFile(identifierId: string, file: File, category: string): Promise { const formData = new FormData() formData.append('file', file) formData.append('entityType', 'member_identifier') formData.append('entityId', identifierId) formData.append('category', category) const res = await fetch('/v1/files', { method: 'POST', headers: { Authorization: `Bearer ${token}` }, body: formData, }) if (!res.ok) return null return (await res.json()).id } async function handleCreateIdentifier(data: Record, files: IdentifierFiles) { setCreateLoading(true) try { const identifier = await identifierMutations.create(memberId, data) const updates: Record = {} if (files.front) { const fileId = await uploadIdFile(identifier.id, files.front, 'front') if (fileId) updates.imageFrontFileId = fileId } if (files.back) { const fileId = await uploadIdFile(identifier.id, files.back, 'back') if (fileId) updates.imageBackFileId = fileId } if (Object.keys(updates).length > 0) await identifierMutations.update(identifier.id, updates) queryClient.invalidateQueries({ queryKey: identifierKeys.all(memberId) }) toast.success('ID added') setAddIdOpen(false) } catch (err) { toast.error(err instanceof Error ? err.message : 'Failed to add ID') } finally { setCreateLoading(false) } } const deleteIdMutation = useMutation({ mutationFn: identifierMutations.delete, onSuccess: () => { queryClient.invalidateQueries({ queryKey: identifierKeys.all(memberId) }) toast.success('ID removed') }, onError: (err) => toast.error(err.message), }) function setTab(t: string) { navigate({ to: '/members/$memberId', params: { memberId }, search: { tab: t } as any }) } if (isLoading) { return (
) } if (!member) return

Member not found

const identifiers = idsData?.data ?? [] const tabs = [ { key: 'details', label: 'Details' }, { key: 'identity', label: 'Identity Documents' }, ...(lessonsEnabled ? [{ key: 'enrollments', label: 'Enrollments' }] : []), ] return (
{/* Header */}

{member.firstName} {member.lastName}

#{member.memberNumber} {member.isMinor && Minor} View Account
{/* Tabs */} {/* Details tab */} {tab === 'details' && (

{member.firstName} {member.lastName}

{member.email ?? 'No email'}

updateMutation.mutate(data)} loading={updateMutation.isPending} />
)} {/* Identity Documents tab */} {tab === 'identity' && (

{identifiers.length} document(s) on file

Add Identity Document
{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.issuedDate && Issued: {id.issuedDate}} {id.expiresAt && Expires: {id.expiresAt}}
{(id.imageFrontFileId || id.imageBackFileId) && }
))}
)}
)} {/* Enrollments tab */} {tab === 'enrollments' && (

{enrollmentsData?.pagination.total ?? 0} enrollment(s)

{hasPermission('lessons.edit') && ( )}
{}} onSort={() => {}} onRowClick={(e) => navigate({ to: '/lessons/enrollments/$enrollmentId', params: { enrollmentId: e.id }, search: {} as any })} />
)}
) }