add school year model

This commit is contained in:
root
2026-06-07 00:52:04 -04:00
parent c4d7a06a17
commit 8e79201a3c
32 changed files with 2031 additions and 213 deletions
+20 -3
View File
@@ -1,10 +1,11 @@
import { useEffect, useState } from 'react'
import { Link, useParams } from 'react-router-dom'
import { Link, useParams, useSearchParams } from 'react-router-dom'
import {
fetchTeacherDashboard,
isTeacherDashboardApiEnabled,
type TeacherDashboardPayload,
} from '../../api/teacherSession'
import { useSchoolYearOptions } from '../../hooks/useSchoolYearOptions'
import { TeacherShell } from './TeacherShell'
import { useTeacherResource } from './useTeacherResource'
@@ -18,6 +19,16 @@ type SentPayload = { sentMessages?: MsgRow[] }
type TeacherMessagePayload = { message?: Record<string, unknown>; thread?: unknown[] }
function appendSchoolYearParam(path: string, schoolYear: string): string {
if (!schoolYear) return path
const [base, hash = ''] = path.split('#', 2)
const [pathname, query = ''] = base.split('?', 2)
const params = new URLSearchParams(query)
if (!params.get('school_year')) params.set('school_year', schoolYear)
const next = `${pathname}?${params.toString()}`
return hash ? `${next}#${hash}` : next
}
/** Every `/app/teacher/*` route so nothing is hidden behind the slim top nav alone. */
const TEACHER_PAGE_GROUPS = [
{
@@ -204,14 +215,20 @@ export function TeacherNoClassesPage() {
/** `teacher/select_semester.php` */
export function TeacherSelectSemesterPage() {
const [searchParams] = useSearchParams()
const { data, loading, error } = useTeacherResource<{
invalidChoice?: string
fallPath?: string
springPath?: string
}>('/select-semester')
const requestedSchoolYear = searchParams.get('school_year')?.trim() ?? ''
const { selectedYear } = useSchoolYearOptions({
preferredYear: requestedSchoolYear,
})
const activeSchoolYear = requestedSchoolYear || selectedYear
const fallTo = data?.fallPath ?? '/app/teacher/scores?semester=Fall'
const springTo = data?.springPath ?? '/app/teacher/scores?semester=Spring'
const fallTo = appendSchoolYearParam(data?.fallPath ?? '/app/teacher/scores?semester=Fall', activeSchoolYear)
const springTo = appendSchoolYearParam(data?.springPath ?? '/app/teacher/scores?semester=Spring', activeSchoolYear)
return (
<TeacherShell title="Choose a semester" loading={loading} error={error}>
+76 -10
View File
@@ -7,6 +7,7 @@ import { useTeacherResource } from './useTeacherResource'
import { postTeacherMultipart } from '../../api/teacherSession'
import { useAuth } from '../../auth/AuthProvider'
import { ApiHttpError, apiFetch } from '../../api/http'
import { useSchoolYearOptions } from '../../hooks/useSchoolYearOptions'
function TeacherApiDumpPage({
title,
@@ -29,6 +30,20 @@ function TeacherApiDumpPage({
)
}
function teacherRouteWithTerm(
path: string,
params: { classSectionId?: number; schoolYear?: string; semester?: string },
): string {
const query = new URLSearchParams()
if (params.classSectionId && params.classSectionId > 0) {
query.set('class_section_id', String(params.classSectionId))
}
if (params.schoolYear) query.set('school_year', params.schoolYear)
if (params.semester) query.set('semester', params.semester)
const value = query.toString()
return value ? `${path}?${value}` : path
}
type CompetitionIndexPayload = {
success?: string
error?: string
@@ -1372,6 +1387,10 @@ export function TeacherScoresPage() {
const [search] = useSearchParams()
const requestedSemester = search.get('semester')?.trim() ?? ''
const requestedSchoolYear = search.get('school_year')?.trim() ?? ''
const { selectedYear: defaultSchoolYear } = useSchoolYearOptions({
preferredYear: requestedSchoolYear,
})
const effectiveRequestedSchoolYear = requestedSchoolYear || defaultSchoolYear
const [payload, setPayload] = useState<TeacherScoresPayload | null>(null)
const [rows, setRows] = useState<TeacherScoresRow[]>([])
@@ -1384,7 +1403,7 @@ export function TeacherScoresPage() {
const [tableSearch, setTableSearch] = useState('')
const resolvedSemester = payload?.semester?.trim() || requestedSemester
const resolvedSchoolYear = payload?.school_year?.trim() || requestedSchoolYear
const resolvedSchoolYear = payload?.school_year?.trim() || effectiveRequestedSchoolYear
const semesterKey = teacherSemesterKey(resolvedSemester)
const termLabel = semesterKey === 'midterm' ? 'Midterm' : semesterKey === 'final' ? 'Final Exam' : 'Term Score'
const termCommentLabel = semesterKey === 'midterm' ? 'Midterm Comment' : semesterKey === 'final' ? 'Final Comment' : 'Term Comment'
@@ -1464,7 +1483,7 @@ export function TeacherScoresPage() {
setWarningLines([])
const params = new URLSearchParams()
if (requestedSemester !== '') params.set('semester', requestedSemester)
if (requestedSchoolYear !== '') params.set('school_year', requestedSchoolYear)
if (effectiveRequestedSchoolYear !== '') params.set('school_year', effectiveRequestedSchoolYear)
const query = params.toString()
const body = await apiFetch<TeacherScoresPayload>(`/api/v1/teacher/scores${query ? `?${query}` : ''}`)
setPayload(body)
@@ -1478,7 +1497,7 @@ export function TeacherScoresPage() {
useEffect(() => {
void load()
}, [requestedSchoolYear, requestedSemester])
}, [effectiveRequestedSchoolYear, requestedSemester])
const visibleRows = useMemo(() => {
const needle = tableSearch.trim().toLowerCase()
@@ -1674,7 +1693,12 @@ export function TeacherScoresPage() {
) : null}
<div className="text-center mb-3">
<Link to="/app/teacher/select-semester" className="btn btn-outline-secondary btn-sm">
<Link
to={teacherRouteWithTerm('/app/teacher/select-semester', {
schoolYear: resolvedSchoolYear,
})}
className="btn btn-outline-secondary btn-sm"
>
Choose another semester
</Link>
</div>
@@ -1807,25 +1831,67 @@ export function TeacherScoresPage() {
</div>
<div className="d-flex flex-wrap gap-2 mt-3">
<Link to={`/app/teacher/add-homework?class_section_id=${classSectionId}`} className="btn btn-secondary">
<Link
to={teacherRouteWithTerm('/app/teacher/add-homework', {
classSectionId,
schoolYear: resolvedSchoolYear,
semester: resolvedSemester,
})}
className="btn btn-secondary"
>
Add Homework
</Link>
<Link to={`/app/teacher/add-quiz?class_section_id=${classSectionId}`} className="btn btn-secondary">
<Link
to={teacherRouteWithTerm('/app/teacher/add-quiz', {
classSectionId,
schoolYear: resolvedSchoolYear,
semester: resolvedSemester,
})}
className="btn btn-secondary"
>
Add Quiz
</Link>
<Link to={`/app/teacher/add-project?class_section_id=${classSectionId}`} className="btn btn-secondary">
<Link
to={teacherRouteWithTerm('/app/teacher/add-project', {
classSectionId,
schoolYear: resolvedSchoolYear,
semester: resolvedSemester,
})}
className="btn btn-secondary"
>
Add Project
</Link>
<Link to={`/app/teacher/add-participation?class_section_id=${classSectionId}`} className="btn btn-secondary">
<Link
to={teacherRouteWithTerm('/app/teacher/add-participation', {
classSectionId,
schoolYear: resolvedSchoolYear,
semester: resolvedSemester,
})}
className="btn btn-secondary"
>
Add Participation
</Link>
{semesterKey === 'midterm' ? (
<Link to={`/app/teacher/add-midterm-exam?class_section_id=${classSectionId}`} className="btn btn-secondary">
<Link
to={teacherRouteWithTerm('/app/teacher/add-midterm-exam', {
classSectionId,
schoolYear: resolvedSchoolYear,
semester: resolvedSemester,
})}
className="btn btn-secondary"
>
Add Midterm
</Link>
) : null}
{semesterKey === 'final' ? (
<Link to={`/app/teacher/add-final-exam?class_section_id=${classSectionId}`} className="btn btn-secondary">
<Link
to={teacherRouteWithTerm('/app/teacher/add-final-exam', {
classSectionId,
schoolYear: resolvedSchoolYear,
semester: resolvedSemester,
})}
className="btn btn-secondary"
>
Add Final
</Link>
) : null}