- 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
88 lines
3.1 KiB
TypeScript
88 lines
3.1 KiB
TypeScript
import { Badge } from '@/components/ui/badge'
|
|
import { Button } from '@/components/ui/button'
|
|
import { Pencil, Trash2 } from 'lucide-react'
|
|
import type { ScheduleSlot, LessonType } from '@/types/lesson'
|
|
|
|
const DAYS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
|
|
|
|
interface Props {
|
|
slots: ScheduleSlot[]
|
|
lessonTypes: LessonType[]
|
|
onEdit: (slot: ScheduleSlot) => void
|
|
onDelete: (slot: ScheduleSlot) => void
|
|
}
|
|
|
|
function formatTime(t: string) {
|
|
const [h, m] = t.split(':').map(Number)
|
|
const ampm = h >= 12 ? 'PM' : 'AM'
|
|
const hour = h % 12 || 12
|
|
return `${hour}:${String(m).padStart(2, '0')} ${ampm}`
|
|
}
|
|
|
|
export function WeeklySlotGrid({ slots, lessonTypes, onEdit, onDelete }: Props) {
|
|
const ltMap = new Map(lessonTypes.map((lt) => [lt.id, lt]))
|
|
|
|
const slotsByDay = DAYS.map((_, day) =>
|
|
slots.filter((s) => s.dayOfWeek === day).sort((a, b) => a.startTime.localeCompare(b.startTime)),
|
|
)
|
|
|
|
const hasAny = slots.length > 0
|
|
|
|
return (
|
|
<div className="grid grid-cols-7 gap-2">
|
|
{DAYS.map((day, idx) => (
|
|
<div key={day} className="min-h-[120px]">
|
|
<div className="text-xs font-semibold text-muted-foreground uppercase tracking-wide text-center mb-2 py-1 border-b">
|
|
{day}
|
|
</div>
|
|
<div className="space-y-2">
|
|
{slotsByDay[idx].map((slot) => {
|
|
const lt = ltMap.get(slot.lessonTypeId)
|
|
return (
|
|
<div
|
|
key={slot.id}
|
|
className="bg-sidebar-accent rounded-md p-2 text-xs group relative"
|
|
>
|
|
<div className="font-medium">{formatTime(slot.startTime)}</div>
|
|
<div className="text-muted-foreground truncate">{lt?.name ?? 'Unknown'}</div>
|
|
{slot.room && <div className="text-muted-foreground">{slot.room}</div>}
|
|
{lt && (
|
|
<Badge variant="outline" className="mt-1 text-[10px] py-0">
|
|
{lt.lessonFormat}
|
|
</Badge>
|
|
)}
|
|
<div className="absolute top-1 right-1 hidden group-hover:flex gap-1">
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="h-5 w-5"
|
|
onClick={() => onEdit(slot)}
|
|
title="Edit"
|
|
>
|
|
<Pencil className="h-3 w-3" />
|
|
</Button>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="h-5 w-5"
|
|
onClick={() => onDelete(slot)}
|
|
title="Delete"
|
|
>
|
|
<Trash2 className="h-3 w-3 text-destructive" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
)
|
|
})}
|
|
</div>
|
|
</div>
|
|
))}
|
|
{!hasAny && (
|
|
<div className="col-span-7 text-center text-sm text-muted-foreground py-8">
|
|
No schedule slots yet — add one to get started.
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|