import { useState } from 'react' import { createFileRoute, useNavigate } from '@tanstack/react-router' import { useQuery } from '@tanstack/react-query' import { format, startOfWeek, endOfWeek, addWeeks, subWeeks, addDays, isSameDay } from 'date-fns' import { sessionListOptions } from '@/api/lessons' import { instructorListOptions } from '@/api/lessons' import { usePagination } from '@/hooks/use-pagination' import { DataTable, type Column } from '@/components/shared/data-table' import { Button } from '@/components/ui/button' import { Badge } from '@/components/ui/badge' import { Input } from '@/components/ui/input' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' import { Search, LayoutList, CalendarDays, ChevronLeft, ChevronRight } from 'lucide-react' import type { LessonSession } from '@/types/lesson' export const Route = createFileRoute('/_authenticated/lessons/sessions/')({ validateSearch: (search: Record) => ({ view: (search.view as 'list' | 'week') || 'list', 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') || 'desc', status: (search.status as string) || undefined, instructorId: (search.instructorId as string) || undefined, }), component: SessionsPage, }) const STATUS_COLORS: Record = { attended: 'bg-green-100 border-green-400 text-green-800', missed: 'bg-red-100 border-red-400 text-red-800', cancelled: 'bg-gray-100 border-gray-300 text-gray-500', makeup: 'bg-purple-100 border-purple-400 text-purple-800', scheduled: 'bg-blue-100 border-blue-400 text-blue-800', } function sessionStatusBadge(status: string) { const variants: Record = { scheduled: 'outline', attended: 'default', missed: 'destructive', makeup: 'secondary', cancelled: 'secondary', } return {status} } function formatTime(t: string) { const [h, m] = t.split(':').map(Number) const ampm = h >= 12 ? 'PM' : 'AM' const hour = h % 12 || 12 return `${hour}:${String(m).padStart(2, '0')} ${ampm}` } const listColumns: Column[] = [ { key: 'scheduled_date', header: 'Date', sortable: true, render: (s) => <>{new Date(s.scheduledDate + 'T00:00:00').toLocaleDateString()}, }, { key: 'scheduled_time', header: 'Time', render: (s) => <>{formatTime(s.scheduledTime)}, }, { key: 'member_name', header: 'Member', render: (s) => {s.memberName ?? '—'}, }, { key: 'instructor_name', header: 'Instructor', render: (s) => <>{s.instructorName ?? '—'}, }, { key: 'lesson_type', header: 'Lesson', render: (s) => <>{s.lessonTypeName ?? '—'}, }, { key: 'status', header: 'Status', sortable: true, render: (s) => sessionStatusBadge(s.status) }, { key: 'notes', header: 'Notes', render: (s) => s.notesCompletedAt ? Notes : null, }, ] const DAYS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'] function SessionsPage() { const navigate = useNavigate() const search = Route.useSearch() const view = search.view ?? 'list' const { params, setPage, setSearch, setSort } = usePagination() const [searchInput, setSearchInput] = useState(params.q ?? '') const [statusFilter, setStatusFilter] = useState(search.status ?? '') const [weekStart, setWeekStart] = useState(() => startOfWeek(new Date(), { weekStartsOn: 0 })) const [weekInstructorId, setWeekInstructorId] = useState(search.instructorId ?? '') const weekEnd = endOfWeek(weekStart, { weekStartsOn: 0 }) function setView(v: 'list' | 'week') { navigate({ to: '/lessons/sessions', search: { ...search, view: v, page: 1 } }) } function handleStatusChange(v: string) { const s = v === 'all' ? '' : v setStatusFilter(s) navigate({ to: '/lessons/sessions', search: { ...search, status: s || undefined, page: 1 } }) } // List query const listQueryParams: Record = { ...params } if (statusFilter) listQueryParams.status = statusFilter const { data: listData, isLoading: listLoading } = useQuery({ ...sessionListOptions(listQueryParams), enabled: view === 'list', }) // Week query const weekQueryParams: Record = { page: 1, limit: 100, sort: 'scheduled_date', order: 'asc', dateFrom: format(weekStart, 'yyyy-MM-dd'), dateTo: format(weekEnd, 'yyyy-MM-dd'), } if (weekInstructorId) weekQueryParams.instructorId = weekInstructorId const { data: weekData } = useQuery({ ...sessionListOptions(weekQueryParams), enabled: view === 'week', }) const { data: instructorsData } = useQuery({ ...instructorListOptions({ page: 1, limit: 100, order: 'asc' }), enabled: view === 'week', }) function handleSearchSubmit(e: React.FormEvent) { e.preventDefault() setSearch(searchInput) } const weekSessions = weekData?.data ?? [] const weekDays = Array.from({ length: 7 }, (_, i) => addDays(weekStart, i)) return (

Sessions

{view === 'list' && ( <>
setSearchInput(e.target.value)} className="pl-9 w-64" />
navigate({ to: '/lessons/sessions/$sessionId', params: { sessionId: s.id }, search: { page: 1, limit: 25, q: undefined, sort: undefined, order: 'asc' as const } })} /> )} {view === 'week' && (
{/* Week nav + instructor filter */}
{format(weekStart, 'MMM d')} – {format(weekEnd, 'MMM d, yyyy')}
{/* Week grid */}
{/* Day headers */} {weekDays.map((day) => { const isToday = isSameDay(day, new Date()) return (

{DAYS[day.getDay()]}

{format(day, 'd')}

) })} {/* Session cells */} {weekDays.map((day) => { const daySessions = weekSessions.filter((s) => s.scheduledDate === format(day, 'yyyy-MM-dd')) const isToday = isSameDay(day, new Date()) return (
{daySessions.length === 0 && (

)} {daySessions.map((s) => ( ))}
) })}
{/* Legend */}
{Object.entries(STATUS_COLORS).map(([status, cls]) => ( {status} ))}
)}
) }