fix teacher, parent and admin pages
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { useEffect, useMemo, useState, type FormEvent, type ReactNode } from 'react'
|
||||
import { Link, useNavigate, useParams, useSearchParams } from 'react-router-dom'
|
||||
import { ApiHttpError } from '../api/http'
|
||||
import {
|
||||
assignTeacherClass,
|
||||
assignStudentClass,
|
||||
@@ -769,7 +770,7 @@ function CalendarEventForm({
|
||||
</div>
|
||||
</div>
|
||||
<div className="card-footer d-flex gap-2 justify-content-end">
|
||||
<button type="button" className="btn btn-secondary" onClick={() => navigate('/app/administrator/calendar_view')}>
|
||||
<button type="button" className="btn btn-secondary" onClick={() => navigate('/app/administrator/events')}>
|
||||
Cancel
|
||||
</button>
|
||||
<button className="btn btn-primary" type="submit" disabled={submitting}>
|
||||
@@ -817,7 +818,7 @@ export function AdminCalendarAddEventPage() {
|
||||
submitting={false}
|
||||
onSubmit={async (payload) => {
|
||||
await createAdministratorCalendarEvent(payload as never)
|
||||
navigate('/app/administrator/calendar_view')
|
||||
navigate('/app/administrator/events')
|
||||
}}
|
||||
/>
|
||||
)
|
||||
@@ -867,12 +868,14 @@ export function AdminCalendarEditPage() {
|
||||
submitting={false}
|
||||
onSubmit={async (payload) => {
|
||||
await updateAdministratorCalendarEvent(Number(eventId), payload)
|
||||
navigate('/app/administrator/calendar_view')
|
||||
navigate('/app/administrator/events')
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
/** School calendar admin — `GET /api/v1/settings/school-calendar/events` (JWT). SPA: `/app/administrator/events` or `calendar_view`. */
|
||||
|
||||
export function AdminCalendarViewPage() {
|
||||
const navigate = useNavigate()
|
||||
const [searchParams, setSearchParams] = useSearchParams()
|
||||
@@ -909,10 +912,13 @@ export function AdminCalendarViewPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<AdminParityShell title="School Calendar">
|
||||
<AdminParityShell title="Event list">
|
||||
{error ? <div className="alert alert-danger">{error}</div> : null}
|
||||
|
||||
<div className="d-flex gap-2 mb-3 justify-content-end">
|
||||
<div className="d-flex gap-2 mb-3 flex-wrap justify-content-between align-items-center">
|
||||
<Link to="/app/administrator/calendar" className="btn btn-outline-secondary">
|
||||
Browse calendar
|
||||
</Link>
|
||||
<Link to={`/app/administrator/calendar_add_event${schoolYear || semester ? `?${new URLSearchParams({ ...(schoolYear ? { school_year: schoolYear } : {}), ...(semester ? { semester } : {}) }).toString()}` : ''}`} className="btn btn-success">
|
||||
Add New Event
|
||||
</Link>
|
||||
@@ -1040,10 +1046,10 @@ export function AdminCalendarPage() {
|
||||
}, [events])
|
||||
|
||||
return (
|
||||
<AdminParityShell title="School Calendar">
|
||||
<AdminParityShell title="School calendar">
|
||||
<div className="d-flex justify-content-end mb-3">
|
||||
<Link to="/app/administrator/calendar_view" className="btn btn-outline-secondary">
|
||||
Manage Events
|
||||
<Link to="/app/administrator/events" className="btn btn-outline-secondary">
|
||||
Manage events (list)
|
||||
</Link>
|
||||
</div>
|
||||
{error ? <div className="alert alert-danger">{error}</div> : null}
|
||||
@@ -2314,7 +2320,7 @@ export function AdminParentProfilePage() {
|
||||
setFamilies((data.data?.families as Array<Record<string, unknown>>) ?? [])
|
||||
setError(null)
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : 'Unable to load parent profiles.')
|
||||
setError(e instanceof ApiHttpError ? e.message : e instanceof Error ? e.message : 'Unable to load parent profiles.')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
@@ -2323,8 +2329,13 @@ export function AdminParentProfilePage() {
|
||||
|
||||
async function loadGuardian(guardianId: number) {
|
||||
setSelectedGuardianId(guardianId)
|
||||
const data = await fetchFamilyAdminIndex({ guardianId })
|
||||
setFamilies((data.data?.families as Array<Record<string, unknown>>) ?? [])
|
||||
setError(null)
|
||||
try {
|
||||
const data = await fetchFamilyAdminIndex({ guardianId })
|
||||
setFamilies((data.data?.families as Array<Record<string, unknown>>) ?? [])
|
||||
} catch (e) {
|
||||
setError(e instanceof ApiHttpError ? e.message : e instanceof Error ? e.message : 'Unable to load families.')
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -2622,6 +2633,106 @@ export function AdminSearchResultsPage() {
|
||||
)
|
||||
}
|
||||
|
||||
/** Section-level promotion totals — `GET /api/v1/students/promotion-totals` (JWT). */
|
||||
export function AdminSectionsPromotionTotalsPage() {
|
||||
const [schoolYear, setSchoolYear] = useState('')
|
||||
const [rows, setRows] = useState<StudentPromotionTotalsRow[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
async function load(year = schoolYear) {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
try {
|
||||
const data = await fetchStudentPromotionTotals(year || undefined)
|
||||
setRows(data.rows ?? [])
|
||||
setSchoolYear(data.year ?? year ?? '')
|
||||
} catch (e) {
|
||||
setError(e instanceof ApiHttpError ? e.message : 'Unable to load promotion totals.')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
void load()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<AdminParityShell title="Section promotion totals">
|
||||
<p className="text-muted small mb-3">
|
||||
Per-class/section student counts from the promotion pipeline. Pair with{' '}
|
||||
<Link to="/app/administrator/sections/auto-distribute">auto-distribute sections</Link> to create
|
||||
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>
|
||||
|
||||
{error ? <div className="alert alert-danger">{error}</div> : null}
|
||||
|
||||
<div className="table-responsive">
|
||||
<table className="table table-sm table-striped align-middle">
|
||||
<thead className="table-light">
|
||||
<tr>
|
||||
<th>Class / section</th>
|
||||
<th className="text-end">Total students</th>
|
||||
<th className="text-muted small">class_id</th>
|
||||
<th className="text-muted small">class_section_id</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{loading ? (
|
||||
<tr>
|
||||
<td colSpan={4} className="text-muted">
|
||||
Loading…
|
||||
</td>
|
||||
</tr>
|
||||
) : rows.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={4} className="text-muted">
|
||||
No promotion totals for this school year.
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
rows.map((row, i) => {
|
||||
const key = String(row.class_section_id ?? row.class_id ?? i)
|
||||
return (
|
||||
<tr key={key}>
|
||||
<td>{row.class_section_name ?? '—'}</td>
|
||||
<td className="text-end">{Number(row.total ?? 0)}</td>
|
||||
<td className="text-muted small">{row.class_id ?? '—'}</td>
|
||||
<td className="text-muted small">{row.class_section_id ?? '—'}</td>
|
||||
</tr>
|
||||
)
|
||||
})
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</AdminParityShell>
|
||||
)
|
||||
}
|
||||
|
||||
export function AdminSectionsAutoDistributePage() {
|
||||
const [schoolYear, setSchoolYear] = useState('')
|
||||
const [studentsPerSection, setStudentsPerSection] = useState(20)
|
||||
@@ -2990,12 +3101,23 @@ export function AdminSubjectCurriculumPage() {
|
||||
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)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [saving, setSaving] = useState(false)
|
||||
|
||||
async function load() {
|
||||
const data = await fetchSubjectCurriculum()
|
||||
setClasses(data.classes ?? [])
|
||||
setEntries(data.entries ?? [])
|
||||
setSubjects(data.subject_labels ?? {})
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
try {
|
||||
const data = await fetchSubjectCurriculum()
|
||||
setClasses(data.classes ?? [])
|
||||
setEntries(data.entries ?? [])
|
||||
setSubjects(data.subject_labels ?? {})
|
||||
} catch (e: unknown) {
|
||||
setError(e instanceof ApiHttpError ? e.message : 'Unable to load curriculum.')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
@@ -3004,6 +3126,8 @@ export function AdminSubjectCurriculumPage() {
|
||||
|
||||
async function submitForm(e: FormEvent) {
|
||||
e.preventDefault()
|
||||
setSaving(true)
|
||||
setError(null)
|
||||
const payload = {
|
||||
class_id: Number(form.class_id),
|
||||
subject: form.subject,
|
||||
@@ -3011,27 +3135,49 @@ export function AdminSubjectCurriculumPage() {
|
||||
unit_title: form.unit_title || null,
|
||||
chapter_name: form.chapter_name,
|
||||
}
|
||||
if (editingId) {
|
||||
await updateSubjectCurriculum(editingId, payload)
|
||||
setMessage('Curriculum entry updated.')
|
||||
} else {
|
||||
await createSubjectCurriculum(payload)
|
||||
setMessage('Curriculum entry created.')
|
||||
try {
|
||||
if (editingId) {
|
||||
await updateSubjectCurriculum(editingId, payload)
|
||||
setMessage('Curriculum entry updated.')
|
||||
} else {
|
||||
await createSubjectCurriculum(payload)
|
||||
setMessage('Curriculum entry created.')
|
||||
}
|
||||
setEditingId(null)
|
||||
setForm({ class_id: '', subject: 'islamic', unit_number: '', unit_title: '', chapter_name: '' })
|
||||
await load()
|
||||
} catch (e: unknown) {
|
||||
setMessage(null)
|
||||
setError(e instanceof ApiHttpError ? e.message : 'Unable to save curriculum entry.')
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
async function removeEntry(id: number) {
|
||||
if (!confirm('Delete this curriculum entry?')) return
|
||||
setError(null)
|
||||
try {
|
||||
await deleteSubjectCurriculum(id)
|
||||
setMessage('Curriculum entry deleted.')
|
||||
await load()
|
||||
} catch (e: unknown) {
|
||||
setMessage(null)
|
||||
setError(e instanceof ApiHttpError ? e.message : 'Unable to delete curriculum entry.')
|
||||
}
|
||||
setEditingId(null)
|
||||
setForm({ class_id: '', subject: 'islamic', unit_number: '', unit_title: '', chapter_name: '' })
|
||||
await load()
|
||||
}
|
||||
|
||||
return (
|
||||
<AdminParityShell title="Subject Curriculum">
|
||||
{message ? <div className="alert alert-info">{message}</div> : null}
|
||||
{error ? <div className="alert alert-danger">{error}</div> : null}
|
||||
<div className="row g-4">
|
||||
<div className="col-lg-5">
|
||||
<div className="card shadow-sm">
|
||||
<div className="card-header">{editingId ? 'Edit curriculum entry' : 'Add new curriculum entry'}</div>
|
||||
<div className="card-body">
|
||||
<form onSubmit={(e) => void submitForm(e)}>
|
||||
<fieldset disabled={loading || saving} className="border-0 m-0 p-0">
|
||||
<div className="mb-3">
|
||||
<label className="form-label">Class</label>
|
||||
<select className="form-select" value={form.class_id} onChange={(e) => setForm((current) => ({ ...current, class_id: e.target.value }))} required>
|
||||
@@ -3049,9 +3195,28 @@ export function AdminSubjectCurriculumPage() {
|
||||
<div className="mb-3"><label className="form-label">Unit title</label><input className="form-control" value={form.unit_title} onChange={(e) => setForm((current) => ({ ...current, unit_title: e.target.value }))} /></div>
|
||||
<div className="mb-3"><label className="form-label">Chapter / Surah</label><input className="form-control" value={form.chapter_name} onChange={(e) => setForm((current) => ({ ...current, chapter_name: e.target.value }))} required /></div>
|
||||
<div className="d-flex gap-2">
|
||||
<button className="btn btn-primary flex-grow-1" type="submit">{editingId ? 'Apply changes' : 'Save curriculum entry'}</button>
|
||||
{editingId ? <button className="btn btn-outline-secondary" type="button" onClick={() => { setEditingId(null); setForm({ class_id: '', subject: 'islamic', unit_number: '', unit_title: '', chapter_name: '' }) }}>Cancel</button> : null}
|
||||
<button
|
||||
className="btn btn-primary flex-grow-1"
|
||||
type="submit"
|
||||
disabled={loading || saving}
|
||||
>
|
||||
{saving ? 'Saving…' : editingId ? 'Apply changes' : 'Save curriculum entry'}
|
||||
</button>
|
||||
{editingId ? (
|
||||
<button
|
||||
className="btn btn-outline-secondary"
|
||||
type="button"
|
||||
disabled={loading || saving}
|
||||
onClick={() => {
|
||||
setEditingId(null)
|
||||
setForm({ class_id: '', subject: 'islamic', unit_number: '', unit_title: '', chapter_name: '' })
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@@ -3063,19 +3228,56 @@ export function AdminSubjectCurriculumPage() {
|
||||
<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>
|
||||
<tbody>
|
||||
{entries.length === 0 ? <tr><td colSpan={6} className="text-muted">No curriculum records yet.</td></tr> : entries.map((entry) => (
|
||||
<tr key={entry.id}>
|
||||
<td>{entry.class_name ?? '—'}</td>
|
||||
<td>{subjects[entry.subject] ?? entry.subject}</td>
|
||||
<td>{entry.unit_number ?? '—'}</td>
|
||||
<td>{entry.unit_title ?? '—'}</td>
|
||||
<td>{entry.chapter_name}</td>
|
||||
<td className="d-flex gap-2">
|
||||
<button className="btn btn-sm btn-outline-primary" type="button" onClick={() => { setEditingId(entry.id); setForm({ class_id: String(entry.class_id), subject: entry.subject, unit_number: entry.unit_number ? String(entry.unit_number) : '', unit_title: entry.unit_title ?? '', chapter_name: entry.chapter_name }) }}>Edit</button>
|
||||
<button className="btn btn-sm btn-outline-danger" type="button" onClick={() => void deleteSubjectCurriculum(entry.id).then(load)}>Delete</button>
|
||||
{loading ? (
|
||||
<tr>
|
||||
<td colSpan={6} className="text-muted">
|
||||
Loading…
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
) : entries.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={6} className="text-muted">
|
||||
No curriculum records yet.
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
entries.map((entry) => (
|
||||
<tr key={entry.id}>
|
||||
<td>{entry.class_name ?? '—'}</td>
|
||||
<td>{subjects[entry.subject] ?? entry.subject}</td>
|
||||
<td>{entry.unit_number ?? '—'}</td>
|
||||
<td>{entry.unit_title ?? '—'}</td>
|
||||
<td>{entry.chapter_name}</td>
|
||||
<td className="d-flex gap-2">
|
||||
<button
|
||||
className="btn btn-sm btn-outline-primary"
|
||||
type="button"
|
||||
disabled={saving}
|
||||
onClick={() => {
|
||||
setEditingId(entry.id)
|
||||
setForm({
|
||||
class_id: String(entry.class_id),
|
||||
subject: entry.subject,
|
||||
unit_number: entry.unit_number ? String(entry.unit_number) : '',
|
||||
unit_title: entry.unit_title ?? '',
|
||||
chapter_name: entry.chapter_name,
|
||||
})
|
||||
}}
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-sm btn-outline-danger"
|
||||
type="button"
|
||||
disabled={saving}
|
||||
onClick={() => void removeEntry(entry.id)}
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -3183,24 +3385,35 @@ export function AdminTeacherClassAssignmentPage() {
|
||||
}
|
||||
|
||||
export function AdminTeacherSubmissionsPage() {
|
||||
const [searchParams] = 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>>({})
|
||||
const [message, setMessage] = useState<string | null>(null)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [semester, setSemester] = useState<string | null>(null)
|
||||
const [schoolYear, setSchoolYear] = useState<string | null>(null)
|
||||
|
||||
async function load() {
|
||||
const data = await fetchTeacherSubmissions()
|
||||
setRows(data.rows ?? [])
|
||||
setSummary(data.summary ?? {})
|
||||
setSemester(data.semester ?? null)
|
||||
setSchoolYear(data.schoolYear ?? null)
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
try {
|
||||
const data = await fetchTeacherSubmissions(searchParams)
|
||||
setRows(data.rows ?? [])
|
||||
setSummary(data.summary ?? {})
|
||||
setSemester(data.semester ?? null)
|
||||
setSchoolYear(data.schoolYear ?? null)
|
||||
} catch (e: unknown) {
|
||||
setError(e instanceof ApiHttpError ? e.message : 'Unable to load teacher submissions.')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
void load()
|
||||
}, [])
|
||||
}, [searchParams])
|
||||
|
||||
async function sendNotifications() {
|
||||
const notify = Object.entries(selected).filter(([, checked]) => checked).map(([id]) => Number(id))
|
||||
@@ -3210,13 +3423,20 @@ export function AdminTeacherSubmissionsPage() {
|
||||
(row.teachers ?? []).map((teacher) => [String(teacher.id), row.missing_items ?? []]),
|
||||
),
|
||||
)
|
||||
const result = await notifyTeacherSubmissions({ notify, missing_items: missingItems })
|
||||
setMessage(result.message ?? 'Notifications sent.')
|
||||
try {
|
||||
const result = await notifyTeacherSubmissions({ notify, missing_items: missingItems })
|
||||
setMessage(result.message ?? 'Notifications sent.')
|
||||
setError(null)
|
||||
} catch (e: unknown) {
|
||||
setMessage(null)
|
||||
setError(e instanceof ApiHttpError ? e.message : 'Unable to send notifications.')
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<AdminParityShell title="Teacher Submission Dashboard">
|
||||
{message ? <div className="alert alert-info">{message}</div> : null}
|
||||
{error ? <div className="alert alert-danger">{error}</div> : null}
|
||||
<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>
|
||||
@@ -3224,15 +3444,31 @@ export function AdminTeacherSubmissionsPage() {
|
||||
<div className="col-sm-6 col-xl-3"><div className="card shadow-sm"><div className="card-body"><div className="text-uppercase small text-muted">Term</div><div className="fs-6 fw-semibold">{`${semester ?? '—'} ${schoolYear ?? ''}`.trim()}</div></div></div></div>
|
||||
</div>
|
||||
<div className="d-flex justify-content-end mb-3">
|
||||
<button className="btn btn-primary" type="button" onClick={() => void sendNotifications()}>Notify Selected Teachers</button>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
type="button"
|
||||
disabled={loading}
|
||||
onClick={() => void sendNotifications()}
|
||||
>
|
||||
Notify Selected Teachers
|
||||
</button>
|
||||
</div>
|
||||
<div className="table-responsive">
|
||||
{loading ? <p className="text-muted">Loading…</p> : null}
|
||||
<table className="table table-striped table-bordered align-middle">
|
||||
<thead className="table-light">
|
||||
<tr><th>Notify</th><th>Class-Section</th><th>Teachers</th><th>Midterm Score</th><th>Midterm Comment</th><th>Participation</th><th>PTAP Comment</th><th>Attendance</th><th>Missing Items</th><th>Student Count</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows.length === 0 ? <tr><td colSpan={10} className="text-muted">No teacher-class assignments found for this term.</td></tr> : rows.map((row) => (
|
||||
{!loading && rows.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={10} className="text-muted">
|
||||
No teacher-class assignments found for this term.
|
||||
</td>
|
||||
</tr>
|
||||
) : null}
|
||||
{!loading
|
||||
? rows.map((row) => (
|
||||
<tr key={String(row.class_section_id ?? row.class_section ?? '')}>
|
||||
<td>
|
||||
<div className="d-flex flex-column gap-1">
|
||||
@@ -3254,7 +3490,8 @@ export function AdminTeacherSubmissionsPage() {
|
||||
<td>{(row.missing_items ?? []).join(', ') || '—'}</td>
|
||||
<td>{row.student_count ?? 0}</td>
|
||||
</tr>
|
||||
))}
|
||||
))
|
||||
: null}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -3310,7 +3547,7 @@ export function AdminCourseMaterialsPage() {
|
||||
<Link className="btn btn-outline-primary" to="/app/administrator/class_section">
|
||||
Class Sections
|
||||
</Link>
|
||||
<Link className="btn btn-outline-primary" to="/app/administrator/calendar_view">
|
||||
<Link className="btn btn-outline-primary" to="/app/administrator/events">
|
||||
Calendar Events
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user