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:
127
packages/admin/src/components/lessons/grading-scale-form.tsx
Normal file
127
packages/admin/src/components/lessons/grading-scale-form.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user