Some checks failed
Build & Release / build (push) Failing after 32s
Newer TanStack Router enforces strict types on search params — 'search: {} as Record<string, unknown>' no longer satisfies routes with validateSearch. Replace all occurrences with the correct search shape for each destination route (pagination defaults for list routes, tab/field defaults for detail routes).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
134 lines
4.9 KiB
TypeScript
134 lines
4.9 KiB
TypeScript
import { useState } from 'react'
|
|
import { createFileRoute, useNavigate } from '@tanstack/react-router'
|
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
|
import { lessonPlanTemplateListOptions, lessonPlanTemplateMutations, lessonPlanTemplateKeys } 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 { Plus, Search, Trash2 } from 'lucide-react'
|
|
import { toast } from 'sonner'
|
|
import { useAuthStore } from '@/stores/auth.store'
|
|
import type { LessonPlanTemplate } from '@/types/lesson'
|
|
|
|
export const Route = createFileRoute('/_authenticated/lessons/templates/')({
|
|
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: TemplatesListPage,
|
|
})
|
|
|
|
const SKILL_LABELS: Record<string, string> = {
|
|
beginner: 'Beginner',
|
|
intermediate: 'Intermediate',
|
|
advanced: 'Advanced',
|
|
all_levels: 'All Levels',
|
|
}
|
|
|
|
const SKILL_VARIANTS: Record<string, 'default' | 'secondary' | 'outline'> = {
|
|
beginner: 'outline',
|
|
intermediate: 'secondary',
|
|
advanced: 'default',
|
|
all_levels: 'outline',
|
|
}
|
|
|
|
const columns: Column<LessonPlanTemplate>[] = [
|
|
{ key: 'name', header: 'Name', sortable: true, render: (t) => <span className="font-medium">{t.name}</span> },
|
|
{ key: 'instrument', header: 'Instrument', render: (t) => <>{t.instrument ?? <span className="text-muted-foreground">—</span>}</> },
|
|
{
|
|
key: 'skill_level', header: 'Level', sortable: true,
|
|
render: (t) => <Badge variant={SKILL_VARIANTS[t.skillLevel] ?? 'outline'}>{SKILL_LABELS[t.skillLevel] ?? t.skillLevel}</Badge>,
|
|
},
|
|
{
|
|
key: 'sections', header: 'Sections',
|
|
render: (t) => <>{t.sections?.length ?? 0} sections</>,
|
|
},
|
|
{
|
|
key: 'is_active', header: 'Status',
|
|
render: (t) => <Badge variant={t.isActive ? 'default' : 'secondary'}>{t.isActive ? 'Active' : 'Inactive'}</Badge>,
|
|
},
|
|
]
|
|
|
|
function TemplatesListPage() {
|
|
const navigate = useNavigate()
|
|
const queryClient = useQueryClient()
|
|
const hasPermission = useAuthStore((s) => s.hasPermission)
|
|
const canAdmin = hasPermission('lessons.admin')
|
|
const { params, setPage, setSearch, setSort } = usePagination()
|
|
const [searchInput, setSearchInput] = useState(params.q ?? '')
|
|
|
|
const { data, isLoading } = useQuery(lessonPlanTemplateListOptions(params))
|
|
|
|
const deleteMutation = useMutation({
|
|
mutationFn: lessonPlanTemplateMutations.delete,
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: lessonPlanTemplateKeys.all })
|
|
toast.success('Template deleted')
|
|
},
|
|
onError: (err) => toast.error(err.message),
|
|
})
|
|
|
|
function handleSearchSubmit(e: React.FormEvent) {
|
|
e.preventDefault()
|
|
setSearch(searchInput)
|
|
}
|
|
|
|
const columnsWithActions: Column<LessonPlanTemplate>[] = [
|
|
...columns,
|
|
...(canAdmin ? [{
|
|
key: 'actions',
|
|
header: '',
|
|
render: (t: LessonPlanTemplate) => (
|
|
<Button variant="ghost" size="sm" onClick={(e) => { e.stopPropagation(); deleteMutation.mutate(t.id) }}>
|
|
<Trash2 className="h-4 w-4 text-destructive" />
|
|
</Button>
|
|
),
|
|
}] : []),
|
|
]
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<div className="flex items-center justify-between">
|
|
<h1 className="text-2xl font-bold">Lesson Plan Templates</h1>
|
|
{canAdmin && (
|
|
<Button onClick={() => navigate({ to: '/lessons/templates/new', search: { page: 1, limit: 25, q: undefined, sort: undefined, order: 'asc' as const } })}>
|
|
<Plus className="mr-2 h-4 w-4" />New Template
|
|
</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 templates..."
|
|
value={searchInput}
|
|
onChange={(e) => setSearchInput(e.target.value)}
|
|
className="pl-9"
|
|
/>
|
|
</div>
|
|
<Button type="submit" variant="secondary">Search</Button>
|
|
</form>
|
|
|
|
<DataTable
|
|
columns={columnsWithActions}
|
|
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={(t) => navigate({ to: '/lessons/templates/$templateId', params: { templateId: t.id }, search: { page: 1, limit: 25, q: undefined, sort: undefined, order: 'asc' as const } })}
|
|
/>
|
|
</div>
|
|
)
|
|
}
|