import { useState } from 'react' import { createFileRoute, useNavigate } from '@tanstack/react-router' import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import { instructorListOptions, instructorMutations, instructorKeys, lessonTypeListOptions, lessonTypeMutations, lessonTypeKeys, gradingScaleListOptions, gradingScaleMutations, gradingScaleKeys, storeClosureListOptions, storeClosureMutations, storeClosureKeys, } from '@/api/lessons' import { usePagination } from '@/hooks/use-pagination' import { DataTable, type Column } from '@/components/shared/data-table' import { InstructorForm } from '@/components/lessons/instructor-form' import { LessonTypeForm } from '@/components/lessons/lesson-type-form' import { GradingScaleForm } from '@/components/lessons/grading-scale-form' import { StoreClosureForm } from '@/components/lessons/store-closure-form' import { Button } from '@/components/ui/button' import { Badge } from '@/components/ui/badge' import { Input } from '@/components/ui/input' import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog' import { Plus, Search, Trash2 } from 'lucide-react' import { toast } from 'sonner' import { useAuthStore } from '@/stores/auth.store' import type { Instructor, LessonType, GradingScale, StoreClosure } from '@/types/lesson' export const Route = createFileRoute('/_authenticated/lessons/schedule/')({ validateSearch: (search: Record) => ({ tab: (search.tab as string) || 'instructors', 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: ScheduleHubPage, }) const TABS = [ { key: 'instructors', label: 'Instructors' }, { key: 'lesson-types', label: 'Lesson Types' }, { key: 'grading-scales', label: 'Grading Scales' }, { key: 'closures', label: 'Store Closures' }, ] function ScheduleHubPage() { const navigate = useNavigate() const search = Route.useSearch() const tab = search.tab const hasPermission = useAuthStore((s) => s.hasPermission) const canAdmin = hasPermission('lessons.admin') function setTab(t: string) { navigate({ to: '/lessons/schedule', search: { ...search, tab: t, page: 1 } }) } return (

Lessons Setup

{TABS.map((t) => ( ))}
{tab === 'instructors' && } {tab === 'lesson-types' && } {tab === 'grading-scales' && } {tab === 'closures' && }
) } // ─── Instructors Tab ────────────────────────────────────────────────────────── const instructorColumns: Column[] = [ { key: 'display_name', header: 'Name', sortable: true, render: (i) => {i.displayName} }, { key: 'instruments', header: 'Instruments', render: (i) => <>{i.instruments?.join(', ') || } }, { key: 'is_active', header: 'Status', sortable: true, render: (i) => {i.isActive ? 'Active' : 'Inactive'}, }, ] function InstructorsTab({ canAdmin, search: _search }: { canAdmin: boolean; search: Record }) { const navigate = useNavigate() const queryClient = useQueryClient() const { params, setPage, setSearch, setSort } = usePagination() const [searchInput, setSearchInput] = useState(params.q ?? '') const [createOpen, setCreateOpen] = useState(false) const { data, isLoading } = useQuery(instructorListOptions(params)) const createMutation = useMutation({ mutationFn: instructorMutations.create, onSuccess: () => { queryClient.invalidateQueries({ queryKey: instructorKeys.all }) toast.success('Instructor created') setCreateOpen(false) }, onError: (err) => toast.error(err.message), }) function handleSearchSubmit(e: React.FormEvent) { e.preventDefault() setSearch(searchInput) } return (
setSearchInput(e.target.value)} className="pl-9" />
{canAdmin && ( Create Instructor )}
navigate({ to: '/lessons/schedule/instructors/$instructorId', params: { instructorId: i.id }, search: { tab: 'overview' } })} />
) } // ─── Lesson Types Tab ───────────────────────────────────────────────────────── const lessonTypeColumns: Column[] = [ { key: 'name', header: 'Name', sortable: true, render: (lt) => {lt.name} }, { key: 'instrument', header: 'Instrument', render: (lt) => <>{lt.instrument ?? } }, { key: 'duration_minutes', header: 'Duration', sortable: true, render: (lt) => <>{lt.durationMinutes} min }, { key: 'lesson_format', header: 'Format', render: (lt) => {lt.lessonFormat} }, { key: 'rate_monthly', header: 'Monthly Rate', render: (lt) => <>{lt.rateMonthly ? `$${lt.rateMonthly}` : } }, { key: 'is_active', header: 'Status', render: (lt) => {lt.isActive ? 'Active' : 'Inactive'} }, ] function LessonTypesTab({ canAdmin, search: _search }: { canAdmin: boolean; search: Record }) { const queryClient = useQueryClient() const { params, setPage, setSearch, setSort } = usePagination() const [searchInput, setSearchInput] = useState(params.q ?? '') const [createOpen, setCreateOpen] = useState(false) const [editTarget, setEditTarget] = useState(null) const { data, isLoading } = useQuery(lessonTypeListOptions(params)) const createMutation = useMutation({ mutationFn: lessonTypeMutations.create, onSuccess: () => { queryClient.invalidateQueries({ queryKey: lessonTypeKeys.all }) toast.success('Lesson type created') setCreateOpen(false) }, onError: (err) => toast.error(err.message), }) const updateMutation = useMutation({ mutationFn: ({ id, data }: { id: string; data: Record }) => lessonTypeMutations.update(id, data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: lessonTypeKeys.all }) toast.success('Lesson type updated') setEditTarget(null) }, onError: (err) => toast.error(err.message), }) const deleteMutation = useMutation({ mutationFn: lessonTypeMutations.delete, onSuccess: () => { queryClient.invalidateQueries({ queryKey: lessonTypeKeys.all }) toast.success('Lesson type removed') }, onError: (err) => toast.error(err.message), }) function handleSearchSubmit(e: React.FormEvent) { e.preventDefault() setSearch(searchInput) } const columnsWithActions: Column[] = [ ...lessonTypeColumns, ...(canAdmin ? [{ key: 'actions', header: '', render: (lt: LessonType) => (
), }] : []), ] return (
setSearchInput(e.target.value)} className="pl-9" />
{canAdmin && ( Create Lesson Type )}
{editTarget && ( { if (!o) setEditTarget(null) }}> Edit Lesson Type updateMutation.mutate({ id: editTarget.id, data })} loading={updateMutation.isPending} /> )}
) } // ─── Grading Scales Tab ─────────────────────────────────────────────────────── const gradingScaleColumns: Column[] = [ { key: 'name', header: 'Name', sortable: true, render: (gs) => {gs.name} }, { key: 'is_default', header: '', render: (gs) => gs.isDefault ? Default : null, }, { key: 'levels', header: 'Levels', render: (gs) => <>{gs.levels?.length ?? 0} }, { key: 'is_active', header: 'Status', render: (gs) => {gs.isActive ? 'Active' : 'Inactive'} }, ] function GradingScalesTab({ canAdmin, search: _search }: { canAdmin: boolean; search: Record }) { const queryClient = useQueryClient() const { params, setPage, setSort } = usePagination() const [createOpen, setCreateOpen] = useState(false) const { data, isLoading } = useQuery(gradingScaleListOptions(params)) const createMutation = useMutation({ mutationFn: gradingScaleMutations.create, onSuccess: () => { queryClient.invalidateQueries({ queryKey: gradingScaleKeys.all }) toast.success('Grading scale created') setCreateOpen(false) }, onError: (err) => toast.error(err.message), }) const deleteMutation = useMutation({ mutationFn: gradingScaleMutations.delete, onSuccess: () => { queryClient.invalidateQueries({ queryKey: gradingScaleKeys.all }) toast.success('Grading scale removed') }, onError: (err) => toast.error(err.message), }) const columnsWithActions: Column[] = [ ...gradingScaleColumns, ...(canAdmin ? [{ key: 'actions', header: '', render: (gs: GradingScale) => ( ), }] : []), ] return (
{canAdmin && ( Create Grading Scale )}
) } // ─── Store Closures Tab ─────────────────────────────────────────────────────── function StoreClosuresTab({ canAdmin }: { canAdmin: boolean }) { const queryClient = useQueryClient() const [createOpen, setCreateOpen] = useState(false) const { data, isLoading } = useQuery(storeClosureListOptions()) const createMutation = useMutation({ mutationFn: storeClosureMutations.create, onSuccess: () => { queryClient.invalidateQueries({ queryKey: storeClosureKeys.all }) toast.success('Store closure added') setCreateOpen(false) }, onError: (err) => toast.error(err.message), }) const deleteMutation = useMutation({ mutationFn: storeClosureMutations.delete, onSuccess: () => { queryClient.invalidateQueries({ queryKey: storeClosureKeys.all }) toast.success('Closure removed') }, onError: (err) => toast.error(err.message), }) const closures: StoreClosure[] = data ?? [] return (
{canAdmin && ( Add Store Closure )}
{isLoading ? (
Loading...
) : closures.length === 0 ? (
No store closures configured.
) : (
{closures.map((c) => (
{c.name}
{new Date(c.startDate + 'T00:00:00').toLocaleDateString()} –{' '} {new Date(c.endDate + 'T00:00:00').toLocaleDateString()}
{canAdmin && ( )}
))}
)}
) }