add school year model
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
))
|
||||
) : (
|
||||
|
||||
@@ -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 : []
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user