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
+30 -15
View File
@@ -2,6 +2,7 @@ import { useEffect, useMemo, useState, type FormEvent, type ReactNode } from 're
import { Link, useNavigate, useParams, useSearchParams } from 'react-router-dom'
import './AdminDailyAttendance.css'
import { ApiHttpError } from '../api/http'
import { useSchoolYearOptions } from '../hooks/useSchoolYearOptions'
import {
assignTeacherClass,
assignStudentClass,
@@ -1050,12 +1051,6 @@ export function AdminCalendarEditPage() {
/** School calendar admin — `GET /api/v1/settings/school-calendar/events` (JWT). SPA: `/app/administrator/events` or `calendar_view`. */
function currentSchoolYear(): string {
const now = new Date()
const y = now.getFullYear()
return now.getMonth() >= 8 ? `${y}-${y + 1}` : `${y - 1}-${y}`
}
export function AdminCalendarViewPage() {
const navigate = useNavigate()
const [searchParams, setSearchParams] = useSearchParams()
@@ -1063,12 +1058,15 @@ export function AdminCalendarViewPage() {
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const schoolYear = searchParams.get('school_year') ?? ''
const { options, currentYear } = useSchoolYearOptions({
preferredYear: schoolYear,
})
useEffect(() => {
if (!schoolYear) {
setSearchParams(new URLSearchParams({ school_year: currentSchoolYear() }), { replace: true })
if (!schoolYear && currentYear) {
setSearchParams(new URLSearchParams({ school_year: currentYear }), { replace: true })
}
}, [])
}, [currentYear, schoolYear, setSearchParams])
async function load() {
setLoading(true)
@@ -1124,14 +1122,27 @@ export function AdminCalendarViewPage() {
>
<div>
<label htmlFor="cal-school-year" className="form-label">School Year</label>
<input id="cal-school-year" name="school_year" className="form-control" defaultValue={schoolYear} placeholder="e.g. 2025-2026" />
<select
id="cal-school-year"
name="school_year"
className="form-select"
defaultValue={schoolYear || currentYear}
>
{options.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
</div>
<div className="d-flex gap-2 align-self-end">
<button className="btn btn-secondary" type="submit">Apply</button>
<button
className="btn btn-outline-secondary"
type="button"
onClick={() => setSearchParams(new URLSearchParams({ school_year: currentSchoolYear() }))}
onClick={() =>
setSearchParams(new URLSearchParams(currentYear ? { school_year: currentYear } : {}))
}
>
Reset
</button>
@@ -1307,6 +1318,10 @@ export function AdminClassAssignmentPage() {
key: 'school_id',
direction: 'asc',
})
const { options, selectedYear: defaultYear } = useSchoolYearOptions({
legacyYears: schoolYears,
preferredYear: selectedYear,
})
async function load(year?: string | null) {
setLoading(true)
@@ -1316,7 +1331,7 @@ export function AdminClassAssignmentPage() {
const classSections = data.classSections ?? []
setSections(classSections)
setSchoolYears(data.schoolYears ?? [])
setSelectedYear(data.selectedYear ?? data.schoolYear ?? '')
setSelectedYear(data.selectedYear ?? data.schoolYear ?? defaultYear)
setSelectedSemester(data.selectedSemester ?? data.semester ?? '')
setExpandedSections(new Set(classSections.length > 0 ? ['0'] : []))
setError(null)
@@ -1397,11 +1412,11 @@ export function AdminClassAssignmentPage() {
<select
className="form-select form-select-sm"
style={{ minWidth: 200 }}
value={selectedYear}
value={selectedYear || defaultYear}
onChange={(event) => setSelectedYear(event.target.value)}
>
{schoolYears.map((year) => (
<option key={year} value={year}>{year}</option>
{options.map((year) => (
<option key={year.value} value={year.value}>{year.label}</option>
))}
</select>
</div>
File diff suppressed because it is too large Load Diff
@@ -1,5 +1,6 @@
import { type FormEvent, useEffect, useMemo, useState } from 'react'
import { Link, useSearchParams } from 'react-router-dom'
import { useSchoolYearOptions } from '../../hooks/useSchoolYearOptions'
import {
fetchAdministratorAdminAttendance,
saveAdministratorAdminAttendance,
@@ -26,6 +27,9 @@ export function AdminsAttendanceFormPage() {
const [saving, setSaving] = useState(false)
const [message, setMessage] = useState<string | null>(null)
const [error, setError] = useState<string | null>(null)
const { options, selectedYear } = useSchoolYearOptions({
preferredYear: schoolYear,
})
useEffect(() => {
let cancelled = false
@@ -143,13 +147,13 @@ export function AdminsAttendanceFormPage() {
>
<div className="col-sm-3">
<label className="form-label">School Year</label>
<input
type="text"
name="school_year"
className="form-control"
defaultValue={schoolYear}
placeholder="e.g. 2025-2026"
/>
<select name="school_year" className="form-select" defaultValue={schoolYear || selectedYear}>
{(options.length > 0 ? options : [{ value: selectedYear, label: selectedYear }]).map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
</div>
<div className="col-sm-2">
<label className="form-label">Semester</label>
+11 -7
View File
@@ -1,6 +1,7 @@
import { type FormEvent, useEffect, useState } from 'react'
import { Link, useSearchParams } from 'react-router-dom'
import { fetchEarlyDismissals, uploadEarlyDismissalSignature } from '../../api/session'
import { useSchoolYearOptions } from '../../hooks/useSchoolYearOptions'
import { apiUrl } from '../../lib/apiOrigin'
import { EARLY_DISMISSALS_NEW_PATH, EARLY_DISMISSALS_PATH } from './attendancePaths'
@@ -20,6 +21,9 @@ export function EarlyDismissalsPage() {
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [flash, setFlash] = useState<string | null>(null)
const { options, selectedYear } = useSchoolYearOptions({
preferredYear: schoolYear,
})
useEffect(() => {
let cancelled = false
@@ -90,13 +94,13 @@ export function EarlyDismissalsPage() {
>
<div className="col-md-4">
<label className="form-label">School Year</label>
<input
type="text"
name="school_year"
className="form-control"
defaultValue={schoolYear}
placeholder="e.g. 2025-2026"
/>
<select name="school_year" className="form-select" defaultValue={schoolYear || selectedYear}>
{(options.length > 0 ? options : [{ value: selectedYear, label: selectedYear }]).map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
</div>
<div className="col-md-3">
<label className="form-label">Semester</label>
@@ -1,5 +1,6 @@
import { type FormEvent, useEffect, useState } from 'react'
import { Link, useSearchParams } from 'react-router-dom'
import { useSchoolYearOptions } from '../../hooks/useSchoolYearOptions'
import { fetchParentAttendanceReportsAdmin } from '../../api/session'
import type { ParentAttendanceAdminReportRow } from '../../api/types'
@@ -14,6 +15,9 @@ export function ParentAttendanceReportsAdminPage() {
const [rows, setRows] = useState<ParentAttendanceAdminReportRow[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const { options, selectedYear } = useSchoolYearOptions({
preferredYear: schoolYear,
})
useEffect(() => {
let cancelled = false
@@ -63,13 +67,13 @@ export function ParentAttendanceReportsAdminPage() {
<form className="row gy-2 gx-3 align-items-end mb-4" onSubmit={apply}>
<div className="col-md-3">
<label className="form-label">School Year</label>
<input
type="text"
name="school_year"
className="form-control"
defaultValue={schoolYear}
placeholder="e.g. 2025-2026"
/>
<select name="school_year" className="form-select" defaultValue={schoolYear || selectedYear}>
{(options.length > 0 ? options : [{ value: selectedYear, label: selectedYear }]).map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
</div>
<div className="col-md-2">
<label className="form-label">Semester</label>
@@ -1,5 +1,6 @@
import { type FormEvent, useEffect, useMemo, useState } from 'react'
import { Link, useSearchParams } from 'react-router-dom'
import { useSchoolYearOptions } from '../../hooks/useSchoolYearOptions'
import { fetchTeacherStaffAttendance, saveTeacherStaffAttendance } from '../../api/session'
import { TEACHER_ATTENDANCE_MONTH_PATH } from './attendancePaths'
import type { TeacherStaffAttendanceRow } from '../../api/types'
@@ -26,6 +27,9 @@ export function TeacherAttendanceFormPage() {
const [saving, setSaving] = useState(false)
const [message, setMessage] = useState<string | null>(null)
const [error, setError] = useState<string | null>(null)
const { options, selectedYear } = useSchoolYearOptions({
preferredYear: schoolYear,
})
useEffect(() => {
let cancelled = false
@@ -136,13 +140,13 @@ export function TeacherAttendanceFormPage() {
<form className="row gy-2 gx-3 align-items-end mb-3" onSubmit={applyFilters}>
<div className="col-sm-3">
<label className="form-label">School Year</label>
<input
type="text"
name="school_year"
className="form-control"
defaultValue={schoolYear}
placeholder="e.g. 2025-2026"
/>
<select name="school_year" className="form-select" defaultValue={schoolYear || selectedYear}>
{(options.length > 0 ? options : [{ value: selectedYear, label: selectedYear }]).map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
</div>
<div className="col-sm-2">
<label className="form-label">Semester</label>
@@ -1,6 +1,7 @@
import { type FormEvent, useEffect, useMemo, useState } from 'react'
import { Link, useSearchParams } from 'react-router-dom'
import { apiUrl } from '../../lib/apiOrigin'
import { useSchoolYearOptions } from '../../hooks/useSchoolYearOptions'
import {
fetchStaffMonthlyAttendanceOverview,
saveStaffMonthlyAttendanceCell,
@@ -104,6 +105,10 @@ export function TeacherAttendanceMonthPage() {
if (ys.length === 0 && schoolYearParam) ys.push(schoolYearParam)
return ys
}, [payload?.schoolYears, schoolYearParam])
const { options } = useSchoolYearOptions({
legacyYears: schoolYears,
preferredYear: schoolYearParam,
})
const hasTeacherSections = (payload?.sections?.length ?? 0) > 0
const hasAdmins = (payload?.admins?.length ?? 0) > 0
@@ -202,9 +207,9 @@ export function TeacherAttendanceMonthPage() {
<div className="col-md-3">
<label className="form-label">School Year</label>
<select name="school_year" className="form-select" defaultValue={schoolYearParam}>
{schoolYears.map((y) => (
<option key={y} value={y}>
{y}
{options.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
+31 -2
View File
@@ -1,5 +1,6 @@
import { type FormEvent, useEffect, useMemo, useState } from 'react'
import { Link, useSearchParams } from 'react-router-dom'
import { useSchoolYearOptions } from '../../hooks/useSchoolYearOptions'
import {
fetchAttendanceViolationsNotified,
fetchAttendanceViolationsPending,
@@ -29,6 +30,9 @@ export function ViolationsPendingPage() {
const [searchParams, setSearchParams] = useSearchParams()
const schoolYear = searchParams.get('school_year') ?? ''
const semester = searchParams.get('semester') ?? ''
const { options, selectedYear } = useSchoolYearOptions({
preferredYear: schoolYear,
})
const filterQuery = useMemo(() => {
const s = searchParams.toString()
@@ -94,7 +98,18 @@ export function ViolationsPendingPage() {
<form className="row g-2 align-items-end" onSubmit={applyFilter}>
<div className="col-auto">
<label htmlFor="pending-school-year" className="form-label small mb-0">School Year</label>
<input id="pending-school-year" name="school_year" className="form-control form-control-sm" defaultValue={schoolYear} />
<select
id="pending-school-year"
name="school_year"
className="form-select form-select-sm"
defaultValue={schoolYear || selectedYear}
>
{(options.length > 0 ? options : [{ value: selectedYear, label: selectedYear }]).map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
</div>
<div className="col-auto">
<label htmlFor="pending-semester" className="form-label small mb-0">Semester</label>
@@ -315,6 +330,9 @@ export function ViolationsNotifiedPage() {
const [searchParams, setSearchParams] = useSearchParams()
const schoolYear = searchParams.get('school_year') ?? ''
const semester = searchParams.get('semester') ?? ''
const { options, selectedYear } = useSchoolYearOptions({
preferredYear: schoolYear,
})
const filterQuery = useMemo(() => {
const s = searchParams.toString()
@@ -399,7 +417,18 @@ export function ViolationsNotifiedPage() {
<form className="row g-2 align-items-end" onSubmit={applyFilter}>
<div className="col-auto">
<label htmlFor="notified-school-year" className="form-label small mb-0">School Year</label>
<input id="notified-school-year" name="school_year" className="form-control form-control-sm" defaultValue={schoolYear} />
<select
id="notified-school-year"
name="school_year"
className="form-select form-select-sm"
defaultValue={schoolYear || selectedYear}
>
{(options.length > 0 ? options : [{ value: selectedYear, label: selectedYear }]).map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
</div>
<div className="col-auto">
<label htmlFor="notified-semester" className="form-label small mb-0">Semester</label>
+11 -18
View File
@@ -1,6 +1,7 @@
import { useCallback, useEffect, useMemo, useState } from 'react'
import { useCallback, useEffect, useState } from 'react'
import { Link, useSearchParams } from 'react-router-dom'
import { ApiHttpError } from '../../api/http'
import { useSchoolYearOptions } from '../../hooks/useSchoolYearOptions'
import { fetchClassPrepIndex, saveClassPrepAdjustment } from '../../api/session'
import type { ClassPrepIndexResponse, ClassPrepSectionRow } from '../../api/types'
@@ -9,14 +10,9 @@ function adjustQty(input: HTMLInputElement, delta: number) {
input.value = String(curr + delta)
}
function defaultSchoolYear() {
const y = new Date().getFullYear()
return `${y}-${y + 1}`
}
export function ClassPrepListPage() {
const [searchParams, setSearchParams] = useSearchParams()
const schoolYear = searchParams.get('school_year') ?? defaultSchoolYear()
const schoolYear = searchParams.get('school_year') ?? ''
const semester = searchParams.get('semester') ?? ''
const [data, setData] = useState<ClassPrepIndexResponse | null>(null)
@@ -24,19 +20,16 @@ export function ClassPrepListPage() {
const [error, setError] = useState<string | null>(null)
const [savingId, setSavingId] = useState<string | null>(null)
const [saveMsg, setSaveMsg] = useState<string | null>(null)
const yearOptions = useMemo(() => {
const out: string[] = []
for (let y = new Date().getFullYear(); y >= 2020; y--) out.push(`${y}-${y + 1}`)
return out
}, [])
const { options, selectedYear } = useSchoolYearOptions({
preferredYear: schoolYear,
})
useEffect(() => {
let cancelled = false
setLoading(true)
setError(null)
fetchClassPrepIndex({
school_year: schoolYear,
school_year: schoolYear || selectedYear,
semester: semester || undefined,
})
.then((d) => {
@@ -115,11 +108,11 @@ export function ClassPrepListPage() {
name="school_year"
id="school_year"
className="form-select form-select-sm w-auto"
defaultValue={schoolYear}
defaultValue={schoolYear || selectedYear}
>
{yearOptions.map((y) => (
<option key={y} value={y}>
{y}
{(options.length > 0 ? options : [{ value: selectedYear, label: selectedYear }]).map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
+29 -25
View File
@@ -1,10 +1,6 @@
import { type FormEvent } from 'react'
import { type FormEvent, useEffect, useState } from 'react'
import { useLocation, useNavigate, useSearchParams } from 'react-router-dom'
function defaultSchoolYear() {
const y = new Date().getFullYear()
return `${y}-${y + 1}`
}
import { useSchoolYearOptions } from '../../hooks/useSchoolYearOptions'
export function AcademicFilterBar({ schoolYears }: { schoolYears?: string[] }) {
const [searchParams] = useSearchParams()
@@ -12,24 +8,26 @@ export function AcademicFilterBar({ schoolYears }: { schoolYears?: string[] }) {
const { pathname } = useLocation()
const schoolYear = searchParams.get('school_year') ?? ''
const semester = searchParams.get('semester') ?? ''
const { options, selectedYear } = useSchoolYearOptions({
legacyYears: schoolYears,
preferredYear: schoolYear,
})
const [yearValue, setYearValue] = useState(schoolYear || selectedYear)
const [semesterValue, setSemesterValue] = useState(semester)
const years =
schoolYears && schoolYears.length > 0
? schoolYears
: (() => {
const out: string[] = []
for (let y = new Date().getFullYear(); y >= 2020; y--) out.push(`${y}-${y + 1}`)
return out
})()
useEffect(() => {
setYearValue(schoolYear || selectedYear)
}, [schoolYear, selectedYear])
useEffect(() => {
setSemesterValue(semester)
}, [semester])
function onSubmit(e: FormEvent<HTMLFormElement>) {
e.preventDefault()
const fd = new FormData(e.currentTarget)
const next = new URLSearchParams()
const sy = String(fd.get('school_year') ?? '')
const sem = String(fd.get('semester') ?? '')
if (sy) next.set('school_year', sy)
if (sem) next.set('semester', sem)
if (yearValue) next.set('school_year', yearValue)
if (semesterValue) next.set('semester', semesterValue)
navigate({ pathname, search: next.toString() ? `?${next}` : '' })
}
@@ -46,11 +44,12 @@ export function AcademicFilterBar({ schoolYears }: { schoolYears?: string[] }) {
name="school_year"
className="form-select form-select-sm"
style={{ minWidth: 180 }}
defaultValue={schoolYear || years[0] || defaultSchoolYear()}
value={yearValue}
onChange={(event) => setYearValue(event.target.value)}
>
{years.map((y) => (
<option key={y} value={y}>
{y}
{options.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
@@ -63,7 +62,8 @@ export function AcademicFilterBar({ schoolYears }: { schoolYears?: string[] }) {
name="semester"
className="form-select form-select-sm"
style={{ minWidth: 140 }}
defaultValue={semester}
value={semesterValue}
onChange={(event) => setSemesterValue(event.target.value)}
>
<option value=""></option>
<option value="Fall">Fall</option>
@@ -77,7 +77,11 @@ export function AcademicFilterBar({ schoolYears }: { schoolYears?: string[] }) {
<button
type="button"
className="btn btn-outline-secondary btn-sm ms-1"
onClick={() => navigate({ pathname, search: '' })}
onClick={() => {
setYearValue(selectedYear)
setSemesterValue('')
navigate({ pathname, search: '' })
}}
>
Reset
</button>
@@ -1,6 +1,7 @@
import { type FormEvent, useEffect, useMemo, useState } from 'react'
import { Link, useLocation, useNavigate, useSearchParams } from 'react-router-dom'
import { ApiHttpError } from '../../api/http'
import { useSchoolYearOptions } from '../../hooks/useSchoolYearOptions'
import {
fetchEnrollmentWithdrawalPage,
postEnrollmentWithdrawalAssignClass,
@@ -88,6 +89,10 @@ export function EnrollmentWithdrawalPage() {
/** Pending edits: undefined means “use row value”. */
const [pendingStatus, setPendingStatus] = useState<Record<number, string>>({})
const [pendingClass, setPendingClass] = useState<Record<number, string>>({})
const { options, selectedYear: defaultYear } = useSchoolYearOptions({
legacyYears: schoolYears,
preferredYear: selectedYear || schoolYearParam,
})
const readOnly = missingYear || !isCurrentYear
@@ -282,8 +287,7 @@ export function EnrollmentWithdrawalPage() {
setBulkWorking(false)
}
const yearSelectValue =
schoolYearParam || selectedYear || (schoolYears.length > 0 ? schoolYears[0] : '')
const yearSelectValue = schoolYearParam || selectedYear || defaultYear
const semesterSelectValue = semesterParam
return (
@@ -326,10 +330,10 @@ export function EnrollmentWithdrawalPage() {
defaultValue={yearSelectValue}
key={`${yearSelectValue}-${semesterSelectValue}`}
>
{schoolYears.length > 0 ? (
schoolYears.map((y) => (
<option key={y} value={y}>
{y}
{options.length > 0 ? (
options.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))
) : (
+21 -6
View File
@@ -2,6 +2,7 @@ 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` */
@@ -11,6 +12,10 @@ function normalizeParticipationScore(value: unknown): number | string | 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
@@ -24,18 +29,24 @@ export function ParticipationPage() {
const [title] = useState('Participation Scores')
const [sectionName, setSectionName] = useState('')
const [semester, setSemester] = useState(searchParams.get('semester') ?? '')
const [schoolYear, setSchoolYear] = useState(searchParams.get('school_year') ?? '')
const [schoolYear, setSchoolYear] = useState(requestedSchoolYear || selectedYear)
const [loading, setLoading] = useState(true)
const [saving, setSaving] = useState(false)
const [error, setError] = useState<string | null>(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(searchParams)
fetchParticipation(participationSearchParams)
.then((p) => {
if (c) return
if (p && typeof p === 'object') {
@@ -57,7 +68,11 @@ export function ParticipationPage() {
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 : (searchParams.get('school_year') ?? ''))
setSchoolYear(
typeof o.school_year === 'string' && o.school_year.trim() !== ''
? o.school_year
: effectiveSchoolYear,
)
}
setError(null)
})
@@ -70,7 +85,7 @@ export function ParticipationPage() {
return () => {
c = true
}
}, [searchParams])
}, [effectiveSchoolYear, participationSearchParams, searchParams])
const visibleRows = useMemo(() => {
const normalized = tableSearch.toLowerCase().trim()
@@ -95,7 +110,7 @@ export function ParticipationPage() {
try {
const body: Record<string, unknown> = {
class_section_id: classSectionId ? Number(classSectionId) : undefined,
school_year: schoolYear || undefined,
school_year: effectiveSchoolYear || undefined,
semester: semester || undefined,
scores: rows.reduce<Record<string, { score: number | string | null }>>((carry, row) => {
carry[String(row.student_id)] = { score: row.score ?? '' }
@@ -103,7 +118,7 @@ export function ParticipationPage() {
}, {}),
}
await postParticipation(body)
const next = await fetchParticipation(searchParams)
const next = await fetchParticipation(participationSearchParams)
if (next && typeof next === 'object') {
const o = next as Record<string, unknown>
const students = Array.isArray(o.students) ? o.students : []
+12 -7
View File
@@ -1,6 +1,7 @@
import { useCallback, useEffect, useState } from 'react'
import { Link, useSearchParams } from 'react-router-dom'
import { ApiHttpError } from '../../api/http'
import { useSchoolYearOptions } from '../../hooks/useSchoolYearOptions'
import { fetchInventorySummary } from '../../api/inventory'
import { AcademicFilterBar } from '../discounts/AcademicFilterBar'
import { INVENTORY_BOOK_BASE } from './inventoryPaths'
@@ -20,6 +21,10 @@ export function InventorySummaryPage() {
const [schoolYears, setSchoolYears] = useState<string[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const { options, currentYear: canonicalCurrentYear } = useSchoolYearOptions({
legacyYears: schoolYears,
preferredYear: selectedYear,
})
const load = useCallback(() => {
setLoading(true)
@@ -94,19 +99,19 @@ export function InventorySummaryPage() {
value={
selectedYear.toLowerCase() === 'all'
? 'all'
: selectedYear === currentYear && !searchParams.get('school_year')
: selectedYear === (currentYear || canonicalCurrentYear) && !searchParams.get('school_year')
? '__current__'
: selectedYear
}
onChange={(e) => onYearFilter(e.target.value)}
>
<option value="__current__">Current ({currentYear || '—'})</option>
<option value="__current__">Current ({currentYear || canonicalCurrentYear || '—'})</option>
<option value="all">All Years</option>
{schoolYears
.filter((y) => y !== currentYear)
.map((y) => (
<option key={y} value={y}>
{y}
{options
.filter((option) => option.value !== (currentYear || canonicalCurrentYear))
.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
@@ -1,6 +1,7 @@
import { type FormEvent, useCallback, useEffect, useState } from 'react'
import { useSearchParams } from 'react-router-dom'
import { ApiHttpError } from '../../api/http'
import { useSchoolYearOptions } from '../../hooks/useSchoolYearOptions'
import { fetchTeacherBookDistribute, postTeacherBookDistribute } from '../../api/inventory'
type BookGroup = { label: string; items: Record<string, unknown>[] }
@@ -23,6 +24,9 @@ export function TeacherBookDistributePage() {
const [loading, setLoading] = useState(true)
const [saving, setSaving] = useState(false)
const [error, setError] = useState<string | null>(null)
const { selectedYear } = useSchoolYearOptions({
preferredYear: schoolYear,
})
const load = useCallback(() => {
setLoading(true)
@@ -74,6 +78,8 @@ export function TeacherBookDistributePage() {
setSelected((prev) => ({ ...prev, [k]: checked }))
}
const effectiveSchoolYear = schoolYear || selectedYear
function checkAll(on: boolean) {
const next: Record<string, boolean> = {}
if (on) {
@@ -179,7 +185,7 @@ export function TeacherBookDistributePage() {
<div className="alert alert-info py-2">
<div className="d-flex justify-content-between flex-wrap gap-2">
<div>
<strong>School Year:</strong> {schoolYear}, <strong>Semester:</strong> {semester}
<strong>School Year:</strong> {effectiveSchoolYear || '—'}, <strong>Semester:</strong> {semester}
</div>
<div>
<strong>On Hand:</strong> {onHand}
@@ -239,7 +245,7 @@ export function TeacherBookDistributePage() {
{String(student.semester ?? semester)}
</td>
<td style={{ textAlign: 'center' }}>
{String(student.school_year ?? schoolYear)}
{String(student.school_year ?? effectiveSchoolYear)}
</td>
<td style={{ textAlign: 'center' }}>
{given ? (
@@ -1,6 +1,7 @@
import { useCallback, useEffect, useMemo, useState } from 'react'
import { Link } from 'react-router-dom'
import { ApiHttpError } from '../../api/http'
import { useSchoolYearOptions } from '../../hooks/useSchoolYearOptions'
import {
fetchInvoiceManagement,
type InvoiceManagementRow,
@@ -37,6 +38,10 @@ export function InvoiceManagementPage() {
const [generating, setGenerating] = useState<number | null>(null)
const [error, setError] = useState<string | null>(null)
const [toast, setToast] = useState<{ ok: boolean; message: string } | null>(null)
const { options, selectedYear: defaultYear } = useSchoolYearOptions({
legacyYears: schoolYears,
preferredYear: schoolYear,
})
const load = useCallback(async (year?: string) => {
setLoading(true)
@@ -47,7 +52,7 @@ export function InvoiceManagementPage() {
const data = await fetchInvoiceManagement(sp)
const years = Array.isArray(data.schoolYears) ? data.schoolYears : []
setSchoolYears(years)
const sel = data.schoolYear ?? years[0] ?? ''
const sel = data.schoolYear ?? years[0] ?? defaultYear
setSchoolYear(sel)
setInvoices(Array.isArray(data.invoices) ? data.invoices : [])
} catch (e: unknown) {
@@ -56,7 +61,7 @@ export function InvoiceManagementPage() {
} finally {
setLoading(false)
}
}, [])
}, [defaultYear])
useEffect(() => {
void load()
@@ -133,9 +138,9 @@ export function InvoiceManagementPage() {
value={schoolYear}
onChange={(e) => void onYearChange(e.target.value)}
>
{schoolYears.map((y) => (
<option key={y} value={y}>
{y}
{options.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
+9 -13
View File
@@ -1,6 +1,7 @@
import { type FormEvent, useCallback, useEffect, useState } from 'react'
import { useLocation, useNavigate, useSearchParams } from 'react-router-dom'
import { ApiHttpError } from '../../api/http'
import { useSchoolYearOptions } from '../../hooks/useSchoolYearOptions'
import {
fetchChargeInvoicesForParent,
fetchExtraChargesPage,
@@ -33,6 +34,10 @@ export function ExtraChargesPage() {
const [invoiceOptions, setInvoiceOptions] = useState<Array<{ id?: number; invoice_number?: string }>>([])
const [modalOpen, setModalOpen] = useState(false)
const [selectedParentId, setSelectedParentId] = useState<string>('')
const { options, selectedYear: defaultYear } = useSchoolYearOptions({
legacyYears: schoolYears,
preferredYear: activeYear || schoolYear,
})
useEffect(() => {
setActiveYear(schoolYear)
@@ -101,15 +106,6 @@ export function ExtraChargesPage() {
let pageTotal = 0
for (const r of rows) pageTotal += Number(r.amount ?? 0)
const years =
schoolYears.length > 0
? schoolYears
: (() => {
const out: string[] = []
for (let y = new Date().getFullYear(); y >= 2020; y--) out.push(`${y}-${y + 1}`)
return out
})()
return (
<div className="container-fluid py-4">
{msg ? <div className="alert alert-info">{msg}</div> : null}
@@ -130,12 +126,12 @@ export function ExtraChargesPage() {
id="schoolYearSel"
className="form-select form-select-sm rounded-pill px-3"
style={{ minWidth: 180 }}
value={activeYear || years[0] || ''}
value={activeYear || defaultYear}
onChange={onYearChange}
>
{years.map((y) => (
<option key={y} value={y}>
{y}
{options.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
+10 -14
View File
@@ -1,6 +1,7 @@
import { type FormEvent, useCallback, useEffect, useMemo, useState } from 'react'
import { Link } from 'react-router-dom'
import { ApiHttpError } from '../../api/http'
import { useSchoolYearOptions } from '../../hooks/useSchoolYearOptions'
import {
downloadFinancialReportCsv,
fetchFinancialReportDetailed,
@@ -29,6 +30,10 @@ export function FinancialReportPage() {
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const [lastRef, setLastRef] = useState<string | null>(null)
const { options, selectedYear: defaultYear } = useSchoolYearOptions({
legacyYears: schoolYears,
preferredYear: schoolYear,
})
const hydrate = useCallback(() => {
setLoading(true)
@@ -133,15 +138,6 @@ export function FinancialReportPage() {
const totalCheck = Number(pt.total_check ?? 0)
const grandTotal = Number(pt.total_all ?? totalCash + totalCredit + totalCheck)
const yearOptions =
schoolYears.length > 0
? schoolYears
: (() => {
const out: string[] = []
for (let y = new Date().getFullYear(); y >= 2020; y--) out.push(`${y}-${y + 1}`)
return out
})()
return (
<div className="container-fluid mt-4">
<h2 className="text-center mt-4 mb-3">Detailed Financial Report</h2>
@@ -158,12 +154,12 @@ export function FinancialReportPage() {
name="school_year"
className="form-select form-select-sm rounded-pill px-3"
style={{ minWidth: 180 }}
value={schoolYear || yearOptions[0] || ''}
value={schoolYear || defaultYear}
onChange={(e) => setSchoolYear(e.target.value)}
>
{yearOptions.map((sy) => (
<option key={sy} value={sy}>
{sy}
{options.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
@@ -221,7 +217,7 @@ export function FinancialReportPage() {
</button>
<Link
className="btn btn-info"
to={`/app/administrator/payment/financial-report-summary?school_year=${encodeURIComponent(schoolYear || yearOptions[0] || '')}`}
to={`/app/administrator/payment/financial-report-summary?school_year=${encodeURIComponent(schoolYear || defaultYear)}`}
>
Display Summary Report
</Link>
@@ -1,6 +1,7 @@
import { type FormEvent, useEffect, useState } from 'react'
import { Link, useSearchParams } from 'react-router-dom'
import { ApiHttpError } from '../../api/http'
import { useSchoolYearOptions } from '../../hooks/useSchoolYearOptions'
import {
downloadFinancialSummaryPdf,
fetchFinancialReportSummary,
@@ -19,6 +20,9 @@ export function FinancialReportSummaryPage() {
const [d, setD] = useState<FinancialSummaryJson | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const { options, selectedYear } = useSchoolYearOptions({
preferredYear: schoolYear || urlYear,
})
function load(y: string) {
setLoading(true)
@@ -87,7 +91,7 @@ export function FinancialReportSummaryPage() {
name="school_year"
className="form-select form-select-sm rounded-pill px-3"
style={{ minWidth: 180 }}
value={schoolYear}
value={schoolYear || selectedYear}
onChange={(e) => {
const y = e.target.value
setSchoolYear(y)
@@ -96,15 +100,11 @@ export function FinancialReportSummaryPage() {
setSearchParams(next)
}}
>
{(() => {
const years: string[] = []
for (let y = new Date().getFullYear(); y >= 2020; y--) years.push(`${y}-${y + 1}`)
return years.map((y) => (
<option key={y} value={y}>
{y}
</option>
))
})()}
{(options.length > 0 ? options : [{ value: selectedYear, label: selectedYear }]).map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
</form>
<div className="d-flex align-items-center gap-2 flex-nowrap flex-shrink-0">
@@ -1,6 +1,7 @@
import { type FormEvent, useEffect, useMemo, useState } from 'react'
import { Link, useSearchParams } from 'react-router-dom'
import { ApiHttpError } from '../../api/http'
import { useSchoolYearOptions } from '../../hooks/useSchoolYearOptions'
import {
downloadBatchCsv,
downloadReimbursementsExport,
@@ -92,6 +93,10 @@ export function ReimbursementsIndexPage() {
}
const schoolYears = data?.schoolYears ?? []
const { options } = useSchoolYearOptions({
legacyYears: schoolYears,
preferredYear: schoolYear,
})
const users = data?.users ?? []
const batchSummaries = (data?.batchSummaries ?? []) as BatchSummaryRow[]
const batchDetails = data?.batchDetails ?? {}
@@ -188,9 +193,9 @@ export function ReimbursementsIndexPage() {
<label className="form-label">School Year</label>
<select name="school_year" className="form-select" defaultValue={schoolYear}>
<option value="">All</option>
{schoolYears.map((sy) => (
<option key={sy} value={sy}>
{sy}
{options.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
+12 -8
View File
@@ -1,6 +1,7 @@
import { type FormEvent, useEffect, useMemo, useState } from 'react'
import { Link, useSearchParams } from 'react-router-dom'
import { ApiHttpError } from '../../api/http'
import { useSchoolYearOptions } from '../../hooks/useSchoolYearOptions'
import { fetchCombinedReport } from '../../api/reportsCombined'
import { REPORT_COMBINED_PATH } from './reportPaths'
@@ -275,6 +276,10 @@ export function CombinedReportPage() {
cursor: 'pointer',
userSelect: 'none' as const,
}
const { options, selectedYear: defaultYear } = useSchoolYearOptions({
preferredYear: selectedYear,
})
const schoolYearOptions = options.length > 0 ? options : [{ value: defaultYear, label: defaultYear }]
return (
<div className="container-fluid py-4">
@@ -289,14 +294,13 @@ export function CombinedReportPage() {
<form className="row g-3 mb-4 align-items-end" onSubmit={applyFilter}>
<div className="col-md-6 col-lg-4">
<label htmlFor="school_year" className="form-label">School Year</label>
<input
id="school_year"
type="text"
name="school_year"
className="form-control"
defaultValue={selectedYear}
placeholder="e.g. 2025-2026"
/>
<select id="school_year" name="school_year" className="form-select" defaultValue={selectedYear || defaultYear}>
{schoolYearOptions.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
</div>
<div className="col-md-6 col-lg-4">
+7 -2
View File
@@ -5,6 +5,7 @@ import {
fetchBadgeScanLogs,
type BadgeScanLogRow,
} from '../../api/badgeScan'
import { useSchoolYearOptions } from '../../hooks/useSchoolYearOptions'
function formatDateTime(raw: string | null): string {
if (!raw) return '—'
@@ -80,6 +81,10 @@ export function BadgeScanLogsPage() {
() => [...new Set(logs.map((r) => r.school_year).filter(Boolean))].sort().reverse() as string[],
[logs],
)
const { options } = useSchoolYearOptions({
legacyYears: schoolYears,
preferredYear: schoolYear,
})
return (
<div className="container-fluid px-0">
@@ -94,8 +99,8 @@ export function BadgeScanLogsPage() {
<label htmlFor="bsl-school-year" className="form-label small mb-0">School Year</label>
<select id="bsl-school-year" name="school_year" className="form-select form-select-sm" defaultValue={schoolYear}>
<option value=""> All </option>
{schoolYears.map((sy) => (
<option key={sy} value={sy}>{sy}</option>
{options.map((option) => (
<option key={option.value} value={option.value}>{option.label}</option>
))}
</select>
</div>
@@ -1,6 +1,7 @@
import { type FormEvent, useEffect, useMemo, useState } from 'react'
import { Link, useSearchParams } from 'react-router-dom'
import { ApiHttpError } from '../../api/http'
import { useSchoolYearOptions } from '../../hooks/useSchoolYearOptions'
import { fetchScorePrediction, type ScorePredictionStudent } from '../../api/scoreAnalysis'
import { FailRiskLabel, StatusBadge, TrophyChanceLabel } from './scorePredictionUi'
@@ -15,6 +16,9 @@ export function ScorePredictionPage() {
const [data, setData] = useState<Awaited<ReturnType<typeof fetchScorePrediction>> | null>(null)
const [error, setError] = useState<string | null>(null)
const { options, selectedYear } = useSchoolYearOptions({
preferredYear: schoolYearParam,
})
useEffect(() => {
setError(null)
@@ -125,6 +129,7 @@ export function ScorePredictionPage() {
}
const syDefault = data?.school_year ?? schoolYearParam
const schoolYearOptions = options.length > 0 ? options : [{ value: selectedYear, label: selectedYear }]
const statusColors = ['#198754', '#dc3545', '#0d6efd', '#ffc107', '#6c757d']
const riskColors = ['#198754', '#ffc107', '#dc3545', '#6610f2', '#adb5bd']
@@ -148,13 +153,13 @@ export function ScorePredictionPage() {
<label htmlFor="school_year" className="form-label">
School Year
</label>
<input
type="text"
className="form-control"
id="school_year"
name="school_year"
defaultValue={syDefault}
/>
<select className="form-select" id="school_year" name="school_year" defaultValue={syDefault || selectedYear}>
{schoolYearOptions.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
</div>
<div className="col-md-6 col-lg-4">
<label htmlFor="class_section_id" className="form-label">
+11 -7
View File
@@ -1,6 +1,7 @@
import { type FormEvent, useEffect, useMemo, useState } from 'react'
import { useSearchParams } from 'react-router-dom'
import { ApiHttpError } from '../../api/http'
import { useSchoolYearOptions } from '../../hooks/useSchoolYearOptions'
import { fetchSlipPreviewList, type SlipPreviewRow } from '../../api/slips'
import { displaySemester, formatPreviewDateKey } from './slipFormatting'
import { SLIPS_PRINT_PATH } from './slipPaths'
@@ -60,6 +61,9 @@ export function SlipPreviewListPage() {
key: 'student_name',
direction: 'asc',
})
const { options, selectedYear } = useSchoolYearOptions({
preferredYear: schoolYear,
})
useEffect(() => {
setError(null)
@@ -159,13 +163,13 @@ export function SlipPreviewListPage() {
<form className="row gy-2 gx-3 align-items-end mb-4" onSubmit={applyFilter}>
<div className="col-md-4">
<label className="form-label">School Year</label>
<input
type="text"
name="school_year"
className="form-control"
defaultValue={schoolYear}
placeholder="e.g. 2025-2026"
/>
<select name="school_year" className="form-select" defaultValue={schoolYear || selectedYear}>
{(options.length > 0 ? options : [{ value: selectedYear, label: selectedYear }]).map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
</div>
<div className="col-md-3">
<label className="form-label">Semester</label>
+15 -5
View File
@@ -1,6 +1,7 @@
import { useEffect, useMemo, useState } from 'react'
import { Link, useSearchParams } from 'react-router-dom'
import { useAuth } from '../../auth/AuthProvider'
import { fetchSchoolYears } from '../../api/schoolYears'
import {
fetchCompetitionScoresIndex,
fetchParentEnrollments,
@@ -16,6 +17,7 @@ import {
semesterHeaderLabel,
sortSemesterKeys,
} from './scoreCardListUtils'
import { canonicalSchoolYearName } from '../../lib/schoolYearOptions'
const LS_CLASS = 'alrahma_teacher_score_card_class_section_id'
const LS_YEAR = 'alrahma_teacher_score_card_school_year'
@@ -41,16 +43,17 @@ async function resolveTeacherScoreCardContext(searchParams: URLSearchParams): Pr
classSectionId: number
schoolYear: string | null
}> {
const canonicalSchoolYear = await resolveDefaultSchoolYear()
const fromUrlC = Number(searchParams.get('class_section_id') ?? '0')
const fromUrlY = searchParams.get('school_year')?.trim() || null
if (fromUrlC > 0) {
return { classSectionId: fromUrlC, schoolYear: fromUrlY }
return { classSectionId: fromUrlC, schoolYear: fromUrlY ?? canonicalSchoolYear }
}
try {
const lsC = Number(localStorage.getItem(LS_CLASS) ?? '0')
const lsY = localStorage.getItem(LS_YEAR)?.trim() || null
if (lsC > 0) {
return { classSectionId: lsC, schoolYear: fromUrlY ?? lsY }
return { classSectionId: lsC, schoolYear: fromUrlY ?? lsY ?? canonicalSchoolYear }
}
} catch {
/* ignore */
@@ -63,16 +66,16 @@ async function resolveTeacherScoreCardContext(searchParams: URLSearchParams): Pr
if (cid > 0) {
try {
localStorage.setItem(LS_CLASS, String(cid))
if (sy) localStorage.setItem(LS_YEAR, sy)
if (sy ?? canonicalSchoolYear) localStorage.setItem(LS_YEAR, sy ?? canonicalSchoolYear ?? '')
} catch {
/* ignore */
}
return { classSectionId: cid, schoolYear: sy }
return { classSectionId: cid, schoolYear: sy ?? canonicalSchoolYear }
}
} catch {
/* ignore */
}
return { classSectionId: 0, schoolYear: fromUrlY }
return { classSectionId: 0, schoolYear: fromUrlY ?? canonicalSchoolYear }
}
function filterStudentsToClassSection(
@@ -86,6 +89,13 @@ function filterStudentsToClassSection(
}
async function resolveDefaultSchoolYear(): Promise<string | null> {
try {
const years = await fetchSchoolYears()
const canonical = canonicalSchoolYearName(years)
if (canonical) return canonical
} catch {
/* ignore */
}
try {
const comp = await fetchCompetitionScoresIndex(null)
const sy = comp.schoolYear != null && String(comp.schoolYear).trim() !== '' ? String(comp.schoolYear).trim() : null
+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}
+9 -3
View File
@@ -1,6 +1,7 @@
import { type FormEvent, type ReactNode, useEffect, useState } from 'react'
import { Link, useSearchParams } from 'react-router-dom'
import { ApiHttpError } from '../../api/http'
import { useSchoolYearOptions } from '../../hooks/useSchoolYearOptions'
import {
fetchTrophyFinal,
fetchTrophyProjection,
@@ -62,14 +63,19 @@ function TrophyFilters({
selectedPercentile: number
onSubmit: (event: FormEvent<HTMLFormElement>) => void
}) {
const { options } = useSchoolYearOptions({
legacyYears: years,
preferredYear: selectedYear,
})
return (
<form className="row g-3 align-items-end mb-4" onSubmit={onSubmit}>
<div className="col-md-4 col-lg-3">
<label className="form-label">School Year</label>
<select className="form-select" name="school_year" defaultValue={selectedYear}>
{(years.length > 0 ? years : [selectedYear]).map((year) => (
<option key={year} value={year}>
{year}
{(options.length > 0 ? options : [{ value: selectedYear, label: selectedYear }]).map((year) => (
<option key={year.value} value={year.value}>
{year.label}
</option>
))}
</select>