'use client' import { useState } from 'react' import { toast } from 'sonner' import { GraduationCap, Plus } from 'lucide-react' import { PageHeader, PageTitle } from '@/components/ui/page-title' import { Button } from '@/components/ui/button' import { Badge } from '@/components/ui/badge' import { Empty, EmptyHeader, EmptyMedia, EmptyTitle, EmptyDescription } from '@/components/ui/empty' import { useConfirm } from '@/components/ui/confirm' import { createClass, updateClass, deleteClass, createSession, updateSession, deleteSession, } from '@/lib/actions' import { SESSION_STATUS_LABEL } from '@/lib/classes-logic' import { SessionAttendance } from '@/components/classes/session-attendance' type Cls = { id: string title: string description: string | null payment_link: string | null capacity: number | null is_active: boolean grants_certification_id: string | null required_form_id: string | null } type Session = { id: string class_id: string starts_at: string ends_at: string | null location: string | null capacity: number | null status: string notes: string | null } type Cert = { id: string; name: string; is_active: boolean } type Form = { id: string; title: string; kind: string } const inputCls = 'w-full bg-background border border-border text-foreground font-sans text-sm rounded px-3 py-2 focus:outline-none focus:border-primary' function emptyClassDraft() { return { title: '', description: '', payment_link: '', capacity: '', grants_certification_id: '', required_form_id: '' } } function emptySessionDraft() { return { starts_at: '', ends_at: '', location: '', capacity: '', notes: '' } } function num(s: string): number | null { const t = s.trim() if (t === '') return null const n = Number(t) return Number.isInteger(n) && n > 0 ? n : null } export function ClassesManageClient({ initialClasses, initialSessions, certs, forms, }: { initialClasses: Cls[] initialSessions: Session[] certs: Cert[] forms: Form[] }) { const confirm = useConfirm() const [classes, setClasses] = useState(initialClasses) const [sessions, setSessions] = useState(initialSessions) const [showNew, setShowNew] = useState(false) const [draft, setDraft] = useState(emptyClassDraft()) const [editId, setEditId] = useState(null) const [editDraft, setEditDraft] = useState(emptyClassDraft()) const [sessForClass, setSessForClass] = useState(null) const [sessDraft, setSessDraft] = useState(emptySessionDraft()) const [rosterFor, setRosterFor] = useState(null) const [busy, setBusy] = useState(false) function certName(id: string | null) { return id ? certs.find(c => c.id === id)?.name ?? null : null } function formName(id: string | null) { return id ? forms.find(f => f.id === id)?.title ?? null : null } async function onCreate(e: React.FormEvent) { e.preventDefault() if (!draft.title.trim()) return toast.error('Title is required') setBusy(true) const res = await createClass({ title: draft.title.trim(), description: draft.description.trim() || null, payment_link: draft.payment_link.trim() || null, capacity: num(draft.capacity), grants_certification_id: draft.grants_certification_id || null, required_form_id: draft.required_form_id || null, }) setBusy(false) if ('error' in res && res.error) return toast.error(res.error) const id = (res as { data: { id: string } }).data.id setClasses(prev => [ { id, title: draft.title.trim(), description: draft.description.trim() || null, payment_link: draft.payment_link.trim() || null, capacity: num(draft.capacity), is_active: true, grants_certification_id: draft.grants_certification_id || null, required_form_id: draft.required_form_id || null, }, ...prev, ]) setDraft(emptyClassDraft()) setShowNew(false) toast.success('Class created') } function startEdit(c: Cls) { setEditId(c.id) setEditDraft({ title: c.title, description: c.description ?? '', payment_link: c.payment_link ?? '', capacity: c.capacity == null ? '' : String(c.capacity), grants_certification_id: c.grants_certification_id ?? '', required_form_id: c.required_form_id ?? '', }) } async function onSaveEdit(id: string) { if (!editDraft.title.trim()) return toast.error('Title is required') setBusy(true) const res = await updateClass({ classId: id, title: editDraft.title.trim(), description: editDraft.description.trim() || null, payment_link: editDraft.payment_link.trim() || null, capacity: num(editDraft.capacity), grants_certification_id: editDraft.grants_certification_id || null, required_form_id: editDraft.required_form_id || null, }) setBusy(false) if ('error' in res && res.error) return toast.error(res.error) setClasses(prev => prev.map(c => c.id === id ? { ...c, title: editDraft.title.trim(), description: editDraft.description.trim() || null, payment_link: editDraft.payment_link.trim() || null, capacity: num(editDraft.capacity), grants_certification_id: editDraft.grants_certification_id || null, required_form_id: editDraft.required_form_id || null, } : c, ), ) setEditId(null) toast.success('Saved') } async function onToggleActive(c: Cls) { const res = await updateClass({ classId: c.id, is_active: !c.is_active }) if ('error' in res && res.error) return toast.error(res.error) setClasses(prev => prev.map(x => (x.id === c.id ? { ...x, is_active: !x.is_active } : x))) toast.success(c.is_active ? 'Archived' : 'Restored') } async function onDeleteClass(c: Cls) { const ok = await confirm({ title: 'Delete class', description: `"${c.title}" will be removed. Only allowed if it has no sessions.`, confirmText: 'Delete', destructive: true, }) if (!ok) return const res = await deleteClass({ classId: c.id }) if ('error' in res && res.error) return toast.error(res.error) setClasses(prev => prev.filter(x => x.id !== c.id)) toast.success('Deleted') } async function onAddSession(e: React.FormEvent, classId: string) { e.preventDefault() if (!sessDraft.starts_at) return toast.error('Start time is required') setBusy(true) const res = await createSession({ classId, starts_at: sessDraft.starts_at, ends_at: sessDraft.ends_at || null, location: sessDraft.location.trim() || null, capacity: num(sessDraft.capacity), notes: sessDraft.notes.trim() || null, }) setBusy(false) if ('error' in res && res.error) return toast.error(res.error) const id = (res as { data: { id: string } }).data.id setSessions(prev => [ ...prev, { id, class_id: classId, starts_at: sessDraft.starts_at, ends_at: sessDraft.ends_at || null, location: sessDraft.location.trim() || null, capacity: num(sessDraft.capacity), status: 'scheduled', notes: sessDraft.notes.trim() || null, }, ]) setSessDraft(emptySessionDraft()) setSessForClass(null) toast.success('Session scheduled') } async function onCancelSession(s: Session) { const ok = await confirm({ title: 'Cancel session', description: 'Registered members will no longer be able to attend.', confirmText: 'Cancel session', destructive: true, }) if (!ok) return const res = await updateSession({ sessionId: s.id, status: 'cancelled' }) if ('error' in res && res.error) return toast.error(res.error) setSessions(prev => prev.map(x => (x.id === s.id ? { ...x, status: 'cancelled' } : x))) toast.success('Session cancelled') } async function onDeleteSession(s: Session) { const ok = await confirm({ title: 'Delete session', description: 'Only allowed if it has no signups. Cancel it instead to keep history.', confirmText: 'Delete', destructive: true, }) if (!ok) return const res = await deleteSession({ sessionId: s.id }) if ('error' in res && res.error) return toast.error(res.error) setSessions(prev => prev.filter(x => x.id !== s.id)) toast.success('Deleted') } return ( <> Manage classes
{showNew && (
setDraft({ ...draft, title: e.target.value })} />