update the new 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 { 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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user