update the new school year model

This commit is contained in:
root
2026-06-07 20:01:56 -04:00
parent 8e79201a3c
commit b6513ab22f
9 changed files with 304 additions and 107 deletions
+64 -54
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 { CiAcademicFilter } from '../components/CiPartials'
import { useSchoolYearOptions } from '../hooks/useSchoolYearOptions'
import {
assignTeacherClass,
@@ -3659,15 +3660,10 @@ export function AdminRemovedStudentsPage() {
return (
<AdminParityShell title="Student Removal">
{error ? <div className="alert alert-danger">{error}</div> : null}
<form className="row g-2 align-items-end mb-3" onSubmit={(e) => { e.preventDefault(); void load() }}>
<div className="col-sm-4 col-lg-3">
<label className="form-label">School Year</label>
<input className="form-control" value={schoolYear} onChange={(e) => setSchoolYear(e.target.value)} placeholder="2025-2026" />
</div>
<div className="col-auto">
<button className="btn btn-primary" type="submit">Apply</button>
</div>
</form>
<CiAcademicFilter
selectedYear={schoolYear}
onApply={({ schoolYear: year }) => void load(year)}
/>
<div className="row g-4">
<div className="col-xl-6">
<div className="card shadow-sm">
@@ -3808,7 +3804,8 @@ export function AdminSearchResultsPage() {
/** Section-level promotion totals — `GET /api/v1/students/promotion-totals` (JWT). */
export function AdminSectionsPromotionTotalsPage() {
const [schoolYear, setSchoolYear] = useState('')
const [searchParams] = useSearchParams()
const [schoolYear, setSchoolYear] = useState(searchParams.get('school_year') ?? '')
const [rows, setRows] = useState<StudentPromotionTotalsRow[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
@@ -3839,26 +3836,14 @@ export function AdminSectionsPromotionTotalsPage() {
sections from these totals.
</p>
<div className="row g-2 align-items-end mb-3 flex-wrap">
<div className="col-sm-4 col-lg-3">
<label className="form-label">School year</label>
<input
className="form-control"
value={schoolYear}
onChange={(e) => setSchoolYear(e.target.value)}
placeholder="e.g. 2025-2026"
/>
</div>
<div className="col-auto">
<button className="btn btn-outline-secondary" type="button" onClick={() => void load()}>
Refresh
</button>
</div>
<div className="col-auto">
<Link className="btn btn-outline-primary" to="/app/administrator/sections/auto-distribute">
Auto-distribute sections
</Link>
</div>
<div className="d-flex flex-wrap align-items-start justify-content-between gap-2 mb-3">
<CiAcademicFilter
selectedYear={schoolYear}
onApply={({ schoolYear: year }) => void load(year)}
/>
<Link className="btn btn-outline-primary" to={`/app/administrator/sections/auto-distribute${schoolYear ? `?school_year=${encodeURIComponent(schoolYear)}` : ''}`}>
Auto-distribute sections
</Link>
</div>
{error ? <div className="alert alert-danger">{error}</div> : null}
@@ -3907,7 +3892,8 @@ export function AdminSectionsPromotionTotalsPage() {
}
export function AdminSectionsAutoDistributePage() {
const [schoolYear, setSchoolYear] = useState('')
const [searchParams] = useSearchParams()
const [schoolYear, setSchoolYear] = useState(searchParams.get('school_year') ?? '')
const [studentsPerSection, setStudentsPerSection] = useState(20)
const [rows, setRows] = useState<StudentPromotionTotalsRow[]>([])
const [loading, setLoading] = useState(true)
@@ -3956,15 +3942,16 @@ export function AdminSectionsAutoDistributePage() {
<AdminParityShell title="Auto-Distribute Students into Sections">
{message ? <div className="alert alert-info">{message}</div> : null}
<div className="row g-2 align-items-end mb-3">
<div className="col-sm-4 col-lg-3">
<label className="form-label">School Year</label>
<input className="form-control" value={schoolYear} onChange={(e) => setSchoolYear(e.target.value)} />
<div className="col-lg-6">
<CiAcademicFilter
selectedYear={schoolYear}
onApply={({ schoolYear: year }) => void load(year)}
/>
</div>
<div className="col-sm-4 col-lg-3">
<label className="form-label">Students per section</label>
<input type="number" min={1} className="form-control" value={studentsPerSection} onChange={(e) => setStudentsPerSection(Number(e.target.value) || 1)} />
</div>
<div className="col-auto"><button className="btn btn-outline-secondary" type="button" onClick={() => void load()}>Refresh Totals</button></div>
<div className="col-auto"><button className="btn btn-primary" type="button" onClick={() => void runAll()} disabled={rows.length === 0}>Generate All</button></div>
</div>
<div className="table-responsive">
@@ -3995,6 +3982,7 @@ export function AdminSectionsAutoDistributePage() {
}
export function AdminStudentClassAssignmentPage() {
const [searchParams] = useSearchParams()
type StudentAssignmentSortKey =
| 'name'
| 'age'
@@ -4003,7 +3991,7 @@ export function AdminStudentClassAssignmentPage() {
| 'assigned_classes'
| 'registration_date'
| 'school_year'
const [schoolYear, setSchoolYear] = useState('')
const [schoolYear, setSchoolYear] = useState(searchParams.get('school_year') ?? '')
const [students, setStudents] = useState<StudentAssignmentRow[]>([])
const [classes, setClasses] = useState<StudentAssignmentOption[]>([])
const [selectedStudentId, setSelectedStudentId] = useState<number | null>(null)
@@ -4118,11 +4106,10 @@ export function AdminStudentClassAssignmentPage() {
{message ? <div className="alert alert-info">{message}</div> : null}
<div className="row g-3 mb-4">
<div className="col-lg-4">
<label className="form-label">School Year</label>
<div className="d-flex gap-2">
<input className="form-control" value={schoolYear} onChange={(e) => setSchoolYear(e.target.value)} />
<button className="btn btn-outline-secondary" type="button" onClick={() => void load()}>Apply</button>
</div>
<CiAcademicFilter
selectedYear={schoolYear}
onApply={({ schoolYear: year }) => void load(year)}
/>
</div>
<div className="col-lg-8">
<div className="card shadow-sm">
@@ -4226,6 +4213,7 @@ export function AdminStudentClassAssignmentPage() {
}
export function AdminStudentProfilesPage() {
const [searchParams] = useSearchParams()
type StudentProfileSortKey =
| 'school_id'
| 'firstname'
@@ -4237,7 +4225,7 @@ export function AdminStudentProfilesPage() {
| 'is_active'
| 'photo_consent'
| 'registration_date'
const [schoolYear, setSchoolYear] = useState('')
const [schoolYear, setSchoolYear] = useState(searchParams.get('school_year') ?? '')
const [students, setStudents] = useState<StudentDirectoryRow[]>([])
const [editing, setEditing] = useState<StudentDirectoryRow | null>(null)
const [parentDetails, setParentDetails] = useState<Record<string, unknown> | null>(null)
@@ -4360,10 +4348,10 @@ export function AdminStudentProfilesPage() {
<AdminParityShell title="Student Profiles">
{message ? <div className="alert alert-info">{message}</div> : null}
<div className="d-flex flex-wrap gap-2 mb-3 justify-content-between">
<div className="d-flex gap-2">
<input className="form-control" style={{ maxWidth: 240 }} value={schoolYear} onChange={(e) => setSchoolYear(e.target.value)} placeholder="School year" />
<button className="btn btn-outline-secondary" type="button" onClick={() => void load()}>Apply</button>
</div>
<CiAcademicFilter
selectedYear={schoolYear}
onApply={({ schoolYear: year }) => void load(year)}
/>
<div className="input-group" style={{ maxWidth: 360 }}>
<input
className="form-control"
@@ -4467,9 +4455,11 @@ export function AdminStudentProfilesPage() {
}
export function AdminSubjectCurriculumPage() {
const [searchParams] = useSearchParams()
const [classes, setClasses] = useState<Array<{ id: number; class_name: string }>>([])
const [entries, setEntries] = useState<SubjectCurriculumEntryRow[]>([])
const [subjects, setSubjects] = useState<Record<string, string>>({})
const [schoolYear, setSchoolYear] = useState(searchParams.get('school_year') ?? '')
const [form, setForm] = useState({ class_id: '', subject: 'islamic', unit_number: '', unit_title: '', chapter_name: '' })
const [editingId, setEditingId] = useState<number | null>(null)
const [message, setMessage] = useState<string | null>(null)
@@ -4477,14 +4467,15 @@ export function AdminSubjectCurriculumPage() {
const [loading, setLoading] = useState(true)
const [saving, setSaving] = useState(false)
async function load() {
async function load(year = schoolYear) {
setLoading(true)
setError(null)
try {
const data = await fetchSubjectCurriculum()
const data = await fetchSubjectCurriculum(year || undefined)
setClasses(data.classes ?? [])
setEntries(data.entries ?? [])
setSubjects(data.subject_labels ?? {})
setSchoolYear(data.school_year ?? year ?? '')
} catch (e: unknown) {
setError(e instanceof ApiHttpError ? e.message : 'Unable to load curriculum.')
} finally {
@@ -4506,6 +4497,7 @@ export function AdminSubjectCurriculumPage() {
unit_number: form.unit_number ? Number(form.unit_number) : null,
unit_title: form.unit_title || null,
chapter_name: form.chapter_name,
school_year: schoolYear || undefined,
}
try {
if (editingId) {
@@ -4543,6 +4535,10 @@ export function AdminSubjectCurriculumPage() {
<AdminParityShell title="Subject Curriculum">
{message ? <div className="alert alert-info">{message}</div> : null}
{error ? <div className="alert alert-danger">{error}</div> : null}
<CiAcademicFilter
selectedYear={schoolYear}
onApply={({ schoolYear: year }) => void load(year)}
/>
<div className="row g-4">
<div className="col-lg-5">
<div className="card shadow-sm">
@@ -4598,17 +4594,17 @@ export function AdminSubjectCurriculumPage() {
<div className="card-header">Current curriculum entries</div>
<div className="table-responsive">
<table className="table table-bordered table-hover align-middle mb-0">
<thead className="table-light"><tr><th>Class</th><th>Subject</th><th>Unit</th><th>Unit title</th><th>Chapter / Surah</th><th>Actions</th></tr></thead>
<thead className="table-light"><tr><th>Class</th><th>School Year</th><th>Subject</th><th>Unit</th><th>Unit title</th><th>Chapter / Surah</th><th>Actions</th></tr></thead>
<tbody>
{loading ? (
<tr>
<td colSpan={6} className="text-muted">
<td colSpan={7} className="text-muted">
Loading
</td>
</tr>
) : entries.length === 0 ? (
<tr>
<td colSpan={6} className="text-muted">
<td colSpan={7} className="text-muted">
No curriculum records yet.
</td>
</tr>
@@ -4616,6 +4612,7 @@ export function AdminSubjectCurriculumPage() {
entries.map((entry) => (
<tr key={entry.id}>
<td>{entry.class_name ?? '—'}</td>
<td>{entry.school_year ?? schoolYear ?? '—'}</td>
<td>{subjects[entry.subject] ?? entry.subject}</td>
<td>{entry.unit_number ?? '—'}</td>
<td>{entry.unit_title ?? '—'}</td>
@@ -4661,7 +4658,8 @@ export function AdminSubjectCurriculumPage() {
}
export function AdminTeacherClassAssignmentPage() {
const [schoolYear, setSchoolYear] = useState('')
const [searchParams] = useSearchParams()
const [schoolYear, setSchoolYear] = useState(searchParams.get('school_year') ?? '')
const [teachers, setTeachers] = useState<TeacherClassAssignmentRow[]>([])
const [classes, setClasses] = useState<TeacherClassOption[]>([])
const [selectedTeacherId, setSelectedTeacherId] = useState<number | null>(null)
@@ -4711,7 +4709,7 @@ export function AdminTeacherClassAssignmentPage() {
<AdminParityShell title="Teacher Class Assignments">
{message ? <div className="alert alert-info">{message}</div> : null}
<div className="row g-3 mb-4">
<div className="col-lg-3"><label className="form-label">School Year</label><div className="d-flex gap-2"><input className="form-control" value={schoolYear} onChange={(e) => setSchoolYear(e.target.value)} /><button className="btn btn-outline-secondary" type="button" onClick={() => void load()}>Apply</button></div></div>
<div className="col-lg-3"><CiAcademicFilter selectedYear={schoolYear} onApply={({ schoolYear: year }) => void load(year)} /></div>
<div className="col-lg-3"><label className="form-label">Teacher</label><select className="form-select" value={selectedTeacherId ?? ''} onChange={(e) => { const id = e.target.value ? Number(e.target.value) : null; setSelectedTeacherId(id); const teacher = teachers.find((row) => row.teacher_id === id); setSelectedRole(String(teacher?.role ?? '')) }}><option value="">Select teacher</option>{teachers.map((teacher) => <option key={teacher.teacher_id} value={teacher.teacher_id}>{teacher.name ?? `${teacher.firstname ?? ''} ${teacher.lastname ?? ''}`.trim()}</option>)}</select></div>
<div className="col-lg-4"><label className="form-label">Class</label><select className="form-select" value={selectedClassId ?? ''} onChange={(e) => setSelectedClassId(e.target.value ? Number(e.target.value) : null)}><option value="">Select class</option>{classes.map((classRow) => <option key={classRow.class_section_id} value={classRow.class_section_id}>{classRow.class_section_name}</option>)}</select></div>
<div className="col-lg-2 d-grid"><button className="btn btn-primary align-self-end" type="button" onClick={() => void submitAssignment()} disabled={!selectedTeacherId || !selectedClassId}>Assign Class</button></div>
@@ -4757,7 +4755,7 @@ export function AdminTeacherClassAssignmentPage() {
}
export function AdminTeacherSubmissionsPage() {
const [searchParams] = useSearchParams()
const [searchParams, setSearchParams] = useSearchParams()
const [rows, setRows] = useState<TeacherSubmissionReportRow[]>([])
const [summary, setSummary] = useState<{ total_items?: number; missing_items?: number; submitted_items?: number; submission_percentage?: number }>({})
const [selected, setSelected] = useState<Record<string, boolean>>({})
@@ -4809,6 +4807,18 @@ export function AdminTeacherSubmissionsPage() {
<AdminParityShell title="Teacher Submission Dashboard">
{message ? <div className="alert alert-info">{message}</div> : null}
{error ? <div className="alert alert-danger">{error}</div> : null}
<CiAcademicFilter
selectedYear={searchParams.get('school_year') ?? schoolYear}
selectedSemester={searchParams.get('semester') ?? semester}
onApply={({ schoolYear: year, semester: sem }) => {
const next = new URLSearchParams(searchParams)
if (year) next.set('school_year', year)
else next.delete('school_year')
if (sem) next.set('semester', sem)
else next.delete('semester')
setSearchParams(next)
}}
/>
<div className="row g-3 mb-4">
<div className="col-sm-6 col-xl-3"><div className="card shadow-sm"><div className="card-body"><div className="text-uppercase small text-muted">Completion</div><div className="fs-3 fw-semibold">{summary.submission_percentage ?? 0}%</div></div></div></div>
<div className="col-sm-6 col-xl-3"><div className="card shadow-sm"><div className="card-body"><div className="text-uppercase small text-muted">Missing Items</div><div className="fs-3 fw-semibold">{summary.missing_items ?? 0}</div></div></div></div>
+21 -4
View File
@@ -4,6 +4,7 @@ import {
type AttendanceManagementDashboard,
} from '../../api/attendanceManagement'
import { ApiHttpError } from '../../api/http'
import { useSchoolYearOptions } from '../../hooks/useSchoolYearOptions'
const today = new Date().toISOString().slice(0, 10)
@@ -13,6 +14,8 @@ function nice(value: string | null | undefined): string {
}
export function BadgeScanningListPage() {
const { options, selectedYear } = useSchoolYearOptions({})
const [schoolYear, setSchoolYear] = useState(selectedYear)
const [date, setDate] = useState(today)
const [status, setStatus] = useState('')
const [q, setQ] = useState('')
@@ -22,13 +25,18 @@ export function BadgeScanningListPage() {
const params = useMemo(
() => ({
school_year: schoolYear,
date,
attendance_status: status,
q,
}),
[date, status, q],
[schoolYear, date, status, q],
)
useEffect(() => {
if (!schoolYear && selectedYear) setSchoolYear(selectedYear)
}, [schoolYear, selectedYear])
async function load() {
setLoading(true)
setError(null)
@@ -73,6 +81,15 @@ export function BadgeScanningListPage() {
<div className="card shadow-sm mb-4">
<div className="card-body">
<div className="row g-3 align-items-end">
<div className="col-md-3">
<label className="form-label">School Year</label>
<select className="form-select" value={schoolYear} onChange={(e) => setSchoolYear(e.target.value)}>
{(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">Date</label>
<input
@@ -83,7 +100,7 @@ export function BadgeScanningListPage() {
/>
</div>
<div className="col-md-3">
<div className="col-md-2">
<label className="form-label">Status</label>
<select
className="form-select"
@@ -97,7 +114,7 @@ export function BadgeScanningListPage() {
</select>
</div>
<div className="col-md-4">
<div className="col-md-3">
<label className="form-label">Search</label>
<input
className="form-control"
@@ -107,7 +124,7 @@ export function BadgeScanningListPage() {
/>
</div>
<div className="col-md-2">
<div className="col-md-1">
<button
className="btn btn-primary w-100"
type="button"