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

@@ -2,7 +2,7 @@ import { createFileRoute } from '@tanstack/react-router'
import { useState } from 'react'
import { getWikiCategories, getWikiPage, type WikiPage } from '@/wiki'
import { Input } from '@/components/ui/input'
import { Search } from 'lucide-react'
import { Search, ChevronDown, ChevronRight } from 'lucide-react'
export const Route = createFileRoute('/_authenticated/help')({
validateSearch: (search: Record<string, unknown>) => ({
@@ -64,6 +64,7 @@ function HelpPage() {
const navigate = Route.useNavigate()
const currentPage = getWikiPage(search.page)
const [searchQuery, setSearchQuery] = useState('')
const [collapsed, setCollapsed] = useState<Record<string, boolean>>({})
const allPages = categories.flatMap((c) => c.pages)
const filteredPages = searchQuery
@@ -79,10 +80,14 @@ function HelpPage() {
setSearchQuery('')
}
function toggleCategory(name: string) {
setCollapsed((prev) => ({ ...prev, [name]: !prev[name] }))
}
return (
<div className="flex gap-6 max-w-5xl">
<div className="flex gap-6 max-w-5xl h-[calc(100vh-8rem)]">
{/* Sidebar */}
<div className="w-56 shrink-0 space-y-4">
<div className="w-56 shrink-0 flex flex-col gap-3">
<div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
@@ -93,9 +98,9 @@ function HelpPage() {
/>
</div>
{filteredPages ? (
<div className="space-y-1">
{filteredPages.length === 0 ? (
<div className="overflow-y-auto flex-1 space-y-1 pr-1">
{filteredPages ? (
filteredPages.length === 0 ? (
<p className="text-sm text-muted-foreground px-2">No results</p>
) : (
filteredPages.map((p) => (
@@ -107,36 +112,47 @@ function HelpPage() {
{p.title}
</button>
))
)}
</div>
) : (
categories.map((cat) => (
<div key={cat.name}>
<h3 className="text-xs font-semibold text-muted-foreground uppercase tracking-wide px-2 mb-1">
{cat.name}
</h3>
<div className="space-y-0.5">
{cat.pages.map((p) => (
)
) : (
categories.map((cat) => {
const isCollapsed = collapsed[cat.name] ?? false
return (
<div key={cat.name}>
<button
key={p.slug}
onClick={() => goToPage(p.slug)}
className={`block w-full text-left px-2 py-1.5 text-sm rounded-md transition-colors ${
search.page === p.slug
? 'bg-accent text-accent-foreground font-medium'
: 'text-muted-foreground hover:bg-accent hover:text-accent-foreground'
}`}
onClick={() => toggleCategory(cat.name)}
className="flex items-center justify-between w-full px-2 py-1 text-xs font-semibold text-muted-foreground uppercase tracking-wide hover:text-foreground transition-colors"
>
{p.title}
{cat.name}
{isCollapsed
? <ChevronRight className="h-3 w-3" />
: <ChevronDown className="h-3 w-3" />}
</button>
))}
</div>
</div>
))
)}
{!isCollapsed && (
<div className="space-y-0.5 mt-0.5">
{cat.pages.map((p) => (
<button
key={p.slug}
onClick={() => goToPage(p.slug)}
className={`block w-full text-left px-2 py-1.5 text-sm rounded-md transition-colors ${
search.page === p.slug
? 'bg-accent text-accent-foreground font-medium'
: 'text-muted-foreground hover:bg-accent hover:text-accent-foreground'
}`}
>
{p.title}
</button>
))}
</div>
)}
</div>
)
})
)}
</div>
</div>
{/* Content */}
<div className="flex-1 min-w-0">
<div className="flex-1 min-w-0 overflow-y-auto">
{currentPage ? (
<div className="prose-sm">{renderMarkdown(currentPage.content)}</div>
) : (