Add lessons module, rate cycles, EC2 deploy scripts, and help content

- Lessons module: lesson types, instructors, schedule slots, enrollments,
  sessions (list + week grid view), lesson plans, grading scales, templates
- Rate cycles: replace monthly_rate with billing_interval + billing_unit on
  enrollments; add weekly/monthly/quarterly rate presets to lesson types and
  schedule slots with auto-fill on enrollment form
- Member detail page: tabbed layout for details, identity documents, enrollments
- Sessions week view: custom 7-column grid replacing react-big-calendar
- Music store seed: instructors, lesson types, slots, enrollments, sessions,
  grading scale, lesson plan template
- Scrollbar styling: themed to match sidebar/app palette
- deploy/: EC2 setup and redeploy scripts, nginx config, systemd service
- Help: add Lessons category (overview, types, instructors, slots, enrollments,
  sessions, plans/grading); collapsible sidebar with independent scroll;
  remove POS/accounting references from docs
This commit is contained in:
Ryan Moon
2026-03-30 18:52:57 -05:00
parent 7680a73d88
commit 5ad27bc196
47 changed files with 6303 additions and 139 deletions

View File

@@ -0,0 +1,91 @@
import { useState } from 'react'
import { createFileRoute, useNavigate } from '@tanstack/react-router'
import { useQuery } from '@tanstack/react-query'
import { lessonPlanListOptions } 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 { Search } from 'lucide-react'
import type { LessonPlan } from '@/types/lesson'
export const Route = createFileRoute('/_authenticated/lessons/plans/')({
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') || 'desc',
}),
component: LessonPlansPage,
})
const columns: Column<LessonPlan>[] = [
{ key: 'title', header: 'Title', sortable: true, render: (p) => <span className="font-medium">{p.title}</span> },
{
key: 'progress', header: 'Progress', sortable: true,
render: (p) => (
<div className="flex items-center gap-2">
<div className="w-24 bg-muted rounded-full h-2">
<div className="bg-primary h-2 rounded-full" style={{ width: `${p.progress}%` }} />
</div>
<span className="text-xs text-muted-foreground">{Math.round(p.progress)}%</span>
</div>
),
},
{
key: 'is_active', header: 'Status',
render: (p) => <Badge variant={p.isActive ? 'default' : 'secondary'}>{p.isActive ? 'Active' : 'Inactive'}</Badge>,
},
{
key: 'created_at', header: 'Created', sortable: true,
render: (p) => <>{new Date(p.createdAt).toLocaleDateString()}</>,
},
]
function LessonPlansPage() {
const navigate = useNavigate()
const { params, setPage, setSearch, setSort } = usePagination()
const [searchInput, setSearchInput] = useState(params.q ?? '')
const { data, isLoading } = useQuery(lessonPlanListOptions(params))
function handleSearchSubmit(e: React.FormEvent) {
e.preventDefault()
setSearch(searchInput)
}
return (
<div className="space-y-6">
<h1 className="text-2xl font-bold">Lesson Plans</h1>
<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 lesson plans..."
value={searchInput}
onChange={(e) => setSearchInput(e.target.value)}
className="pl-9"
/>
</div>
<Button type="submit" variant="secondary">Search</Button>
</form>
<DataTable
columns={columns}
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={(p) => navigate({ to: '/lessons/plans/$planId', params: { planId: p.id }, search: {} as any })}
/>
</div>
)
}