import { type FormEvent, useEffect, useMemo, useState } from 'react' import { Link, useSearchParams } from 'react-router-dom' import { ApiHttpError } from '../../api/http' import { fetchParticipation, postParticipation } from '../../api/grading' import { useSchoolYearOptions } from '../../hooks/useSchoolYearOptions' import { GRADING_PATH } from './gradingPaths' /** CI `grading/participation.php` */ function normalizeParticipationScore(value: unknown): number | string | null { return typeof value === 'number' || typeof value === 'string' ? value : null } export function ParticipationPage() { const [searchParams] = useSearchParams() const requestedSchoolYear = searchParams.get('school_year') ?? '' const { selectedYear } = useSchoolYearOptions({ preferredYear: requestedSchoolYear, }) const [rows, setRows] = useState< Array<{ student_id: number school_id?: string | null firstname?: string | null lastname?: string | null score?: number | string | null }> >([]) const [locked, setLocked] = useState(false) const [title] = useState('Participation Scores') const [sectionName, setSectionName] = useState('') const [semester, setSemester] = useState(searchParams.get('semester') ?? '') const [schoolYear, setSchoolYear] = useState(requestedSchoolYear || selectedYear) const [loading, setLoading] = useState(true) const [saving, setSaving] = useState(false) const [error, setError] = useState(null) const [tableSearch, setTableSearch] = useState('') const classSectionId = searchParams.get('class_section_id') ?? '' const effectiveSchoolYear = schoolYear || selectedYear const participationSearchParams = useMemo(() => { const next = new URLSearchParams(searchParams) if (effectiveSchoolYear) next.set('school_year', effectiveSchoolYear) return next }, [effectiveSchoolYear, searchParams]) useEffect(() => { let c = false setLoading(true) fetchParticipation(participationSearchParams) .then((p) => { if (c) return if (p && typeof p === 'object') { const o = p as Record const students = Array.isArray(o.students) ? o.students : [] setRows( students.map((row) => { const student = row as Record const scores = (student.scores ?? {}) as Record return { student_id: Number(student.student_id ?? 0), school_id: typeof student.school_id === 'string' ? student.school_id : null, firstname: typeof student.firstname === 'string' ? student.firstname : null, lastname: typeof student.lastname === 'string' ? student.lastname : null, score: normalizeParticipationScore(scores.score), } }), ) setLocked(Boolean(o.scores_locked ?? o.scoresLocked ?? o.locked)) setSectionName(typeof o.class_section_name === 'string' ? o.class_section_name : '') setSemester(typeof o.semester === 'string' ? o.semester : (searchParams.get('semester') ?? '')) setSchoolYear( typeof o.school_year === 'string' && o.school_year.trim() !== '' ? o.school_year : effectiveSchoolYear, ) } setError(null) }) .catch((e: unknown) => { if (!c) setError(e instanceof ApiHttpError ? e.message : 'Unable to load participation.') }) .finally(() => { if (!c) setLoading(false) }) return () => { c = true } }, [effectiveSchoolYear, participationSearchParams, searchParams]) const visibleRows = useMemo(() => { const normalized = tableSearch.toLowerCase().trim() if (normalized === '') return rows return rows.filter((row) => `${row.school_id ?? ''} ${row.firstname ?? ''} ${row.lastname ?? ''}`.toLowerCase().includes(normalized), ) }, [rows, tableSearch]) function setScore(i: number, value: string) { setRows((prev) => { const next = [...prev] next[i] = { ...next[i], score: value === '' ? null : Number(value) } return next }) } async function onSubmit(e: FormEvent) { e.preventDefault() setSaving(true) setError(null) try { const body: Record = { class_section_id: classSectionId ? Number(classSectionId) : undefined, school_year: effectiveSchoolYear || undefined, semester: semester || undefined, scores: rows.reduce>((carry, row) => { carry[String(row.student_id)] = { score: row.score ?? '' } return carry }, {}), } await postParticipation(body) const next = await fetchParticipation(participationSearchParams) if (next && typeof next === 'object') { const o = next as Record const students = Array.isArray(o.students) ? o.students : [] setRows( students.map((row) => { const student = row as Record const scores = (student.scores ?? {}) as Record return { student_id: Number(student.student_id ?? 0), school_id: typeof student.school_id === 'string' ? student.school_id : null, firstname: typeof student.firstname === 'string' ? student.firstname : null, lastname: typeof student.lastname === 'string' ? student.lastname : null, score: normalizeParticipationScore(scores.score), } }), ) } } catch (err: unknown) { setError(err instanceof ApiHttpError ? err.message : 'Save failed.') } finally { setSaving(false) } } const qs = searchParams.toString() return (

{title}

{(sectionName || (classSectionId ? `Section ${classSectionId}` : 'Section'))} • {semester || '—'} • {schoolYear || '—'}

Grading hub
{locked ? (
Scores are locked for this selection.
) : null} {error ?
{error}
: null} {loading ?

Loading…

: null} {!loading && rows.length > 0 ? (
void onSubmit(ev)}>
Show entries
setTableSearch(event.target.value)} />
{visibleRows.map((row, i) => ( ))} {visibleRows.length === 0 ? ( ) : null}
# School ID First Name Last Name Score
{i + 1} {row.school_id ?? '—'} {row.firstname ?? '—'} {row.lastname ?? '—'} { const originalIndex = rows.findIndex((candidate) => candidate.student_id === row.student_id) if (originalIndex >= 0) setScore(originalIndex, ev.target.value) }} />
No students match the current search.
) : null} {!loading && rows.length === 0 ? (
No students for this filter.
) : null}
) }