- 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
128 lines
4.6 KiB
TypeScript
128 lines
4.6 KiB
TypeScript
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>
|
|
)
|
|
}
|