Files
lunarfront-app/packages/admin/src/components/lessons/weekly-slot-grid.tsx
Ryan Moon 5ad27bc196 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
2026-03-30 18:52:57 -05:00

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>
)
}