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/grade-entry-dialog.tsx
Normal file
127
packages/admin/src/components/lessons/grade-entry-dialog.tsx
Normal file
@@ -0,0 +1,127 @@
|
||||
import { useState } from 'react'
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { gradingScaleAllOptions, lessonPlanItemMutations, lessonPlanItemGradeHistoryOptions, lessonPlanItemKeys } from '@/api/lessons'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||||
import { toast } from 'sonner'
|
||||
import type { LessonPlanItem } from '@/types/lesson'
|
||||
|
||||
interface Props {
|
||||
item: LessonPlanItem
|
||||
open: boolean
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
export function GradeEntryDialog({ item, open, onClose }: Props) {
|
||||
const queryClient = useQueryClient()
|
||||
const [selectedScaleId, setSelectedScaleId] = useState(item.gradingScaleId ?? '')
|
||||
const [selectedValue, setSelectedValue] = useState('')
|
||||
const [gradeNotes, setGradeNotes] = useState('')
|
||||
|
||||
const { data: scales } = useQuery(gradingScaleAllOptions())
|
||||
const { data: history } = useQuery(lessonPlanItemGradeHistoryOptions(item.id))
|
||||
|
||||
const selectedScale = scales?.find((s) => s.id === selectedScaleId)
|
||||
|
||||
const gradeMutation = useMutation({
|
||||
mutationFn: () =>
|
||||
lessonPlanItemMutations.addGrade(item.id, {
|
||||
gradingScaleId: selectedScaleId || undefined,
|
||||
gradeValue: selectedValue,
|
||||
notes: gradeNotes || undefined,
|
||||
}),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: lessonPlanItemKeys.gradeHistory(item.id) })
|
||||
toast.success('Grade recorded')
|
||||
setSelectedValue('')
|
||||
setGradeNotes('')
|
||||
},
|
||||
onError: (err) => toast.error(err.message),
|
||||
})
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={(o) => { if (!o) onClose() }}>
|
||||
<DialogContent className="max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Grade: {item.title}</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label>Grading Scale</Label>
|
||||
<Select value={selectedScaleId || 'none'} onValueChange={(v) => { setSelectedScaleId(v === 'none' ? '' : v); setSelectedValue('') }}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="No scale (freeform)" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="none">No scale (freeform)</SelectItem>
|
||||
{(scales ?? []).map((s) => (
|
||||
<SelectItem key={s.id} value={s.id}>{s.name}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>Grade Value *</Label>
|
||||
{selectedScale ? (
|
||||
<Select value={selectedValue} onValueChange={setSelectedValue}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select grade..." />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{[...selectedScale.levels].sort((a, b) => a.sortOrder - b.sortOrder).map((level) => (
|
||||
<SelectItem key={level.id} value={level.value}>
|
||||
{level.value} — {level.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
) : (
|
||||
<input
|
||||
className="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-xs transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
||||
value={selectedValue}
|
||||
onChange={(e) => setSelectedValue(e.target.value)}
|
||||
placeholder="Enter grade (e.g. A, Pass, 85)"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>Notes</Label>
|
||||
<Textarea value={gradeNotes} onChange={(e) => setGradeNotes(e.target.value)} rows={2} />
|
||||
</div>
|
||||
|
||||
<Button
|
||||
onClick={() => gradeMutation.mutate()}
|
||||
disabled={!selectedValue || gradeMutation.isPending}
|
||||
className="w-full"
|
||||
>
|
||||
{gradeMutation.isPending ? 'Recording...' : 'Record Grade'}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Grade History */}
|
||||
{(history ?? []).length > 0 && (
|
||||
<div className="border-t pt-4 space-y-2">
|
||||
<p className="text-sm font-medium text-muted-foreground">Grade History</p>
|
||||
<div className="space-y-2 max-h-40 overflow-y-auto">
|
||||
{[...history!].reverse().map((h) => (
|
||||
<div key={h.id} className="flex items-start justify-between text-sm">
|
||||
<div>
|
||||
<span className="font-medium">{h.gradeValue}</span>
|
||||
{h.notes && <p className="text-xs text-muted-foreground">{h.notes}</p>}
|
||||
</div>
|
||||
<span className="text-xs text-muted-foreground">{new Date(h.createdAt).toLocaleDateString()}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user