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,127 @@
import { useState } from 'react'
import { useForm } from 'react-hook-form'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Textarea } from '@/components/ui/textarea'
import { Trash2, Plus } from 'lucide-react'
interface LevelRow {
value: string
label: string
numericValue: string
colorHex: string
}
interface Props {
onSubmit: (data: Record<string, unknown>) => void
loading?: boolean
}
const DEFAULT_LEVELS: LevelRow[] = [
{ value: 'A', label: 'Excellent', numericValue: '4', colorHex: '#22c55e' },
{ value: 'B', label: 'Good', numericValue: '3', colorHex: '#84cc16' },
{ value: 'C', label: 'Developing', numericValue: '2', colorHex: '#eab308' },
{ value: 'D', label: 'Beginning', numericValue: '1', colorHex: '#f97316' },
]
export function GradingScaleForm({ onSubmit, loading }: Props) {
const { register, handleSubmit } = useForm({
defaultValues: { name: '', description: '', isDefault: false },
})
const [levels, setLevels] = useState<LevelRow[]>(DEFAULT_LEVELS)
function addLevel() {
setLevels((prev) => [...prev, { value: '', label: '', numericValue: String(prev.length + 1), colorHex: '' }])
}
function removeLevel(idx: number) {
setLevels((prev) => prev.filter((_, i) => i !== idx))
}
function updateLevel(idx: number, field: keyof LevelRow, value: string) {
setLevels((prev) => prev.map((l, i) => (i === idx ? { ...l, [field]: value } : l)))
}
function handleFormSubmit(data: { name: string; description: string; isDefault: boolean }) {
onSubmit({
name: data.name,
description: data.description || undefined,
isDefault: data.isDefault,
levels: levels.map((l, i) => ({
value: l.value,
label: l.label,
numericValue: Number(l.numericValue) || i + 1,
colorHex: l.colorHex || undefined,
sortOrder: i,
})),
})
}
return (
<form onSubmit={handleSubmit(handleFormSubmit)} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="gs-name">Name *</Label>
<Input id="gs-name" {...register('name')} placeholder="e.g. RCM Performance Scale" required />
</div>
<div className="space-y-2">
<Label htmlFor="gs-desc">Description</Label>
<Textarea id="gs-desc" {...register('description')} rows={2} />
</div>
<div className="flex items-center gap-2">
<input type="checkbox" id="gs-default" {...register('isDefault')} className="h-4 w-4" />
<Label htmlFor="gs-default">Set as default scale</Label>
</div>
<div className="space-y-2">
<div className="flex items-center justify-between">
<Label>Grade Levels</Label>
<Button type="button" variant="outline" size="sm" onClick={addLevel}>
<Plus className="h-3 w-3 mr-1" />Add Level
</Button>
</div>
<div className="space-y-2 max-h-64 overflow-y-auto pr-1">
{levels.map((level, idx) => (
<div key={idx} className="grid grid-cols-[1fr_2fr_1fr_auto_auto] gap-2 items-center">
<Input
placeholder="Value"
value={level.value}
onChange={(e) => updateLevel(idx, 'value', e.target.value)}
required
/>
<Input
placeholder="Label"
value={level.label}
onChange={(e) => updateLevel(idx, 'label', e.target.value)}
required
/>
<Input
type="number"
placeholder="Score"
value={level.numericValue}
onChange={(e) => updateLevel(idx, 'numericValue', e.target.value)}
/>
<input
type="color"
value={level.colorHex || '#888888'}
onChange={(e) => updateLevel(idx, 'colorHex', e.target.value)}
className="h-9 w-9 rounded border border-input cursor-pointer"
title="Color"
/>
<Button type="button" variant="ghost" size="icon" onClick={() => removeLevel(idx)} className="h-9 w-9">
<Trash2 className="h-4 w-4 text-destructive" />
</Button>
</div>
))}
</div>
{levels.length === 0 && (
<p className="text-sm text-muted-foreground text-center py-2">No levels add at least one.</p>
)}
</div>
<Button type="submit" disabled={loading || levels.length === 0} className="w-full">
{loading ? 'Saving...' : 'Create Grading Scale'}
</Button>
</form>
)
}