fix teacher, parent and admin pages
This commit is contained in:
@@ -0,0 +1,684 @@
|
||||
import { Fragment, useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { Link, useSearchParams } from 'react-router-dom'
|
||||
import { ApiHttpError, apiFetch } from '../../api/http'
|
||||
import { TeacherShell } from './TeacherShell'
|
||||
import { useTeacherResource } from './useTeacherResource'
|
||||
|
||||
const CUSTOM_UNIT = 'Custom'
|
||||
|
||||
export type CurriculumRow = {
|
||||
unit_number?: number | null
|
||||
unit_title?: string | null
|
||||
chapter_name?: string | null
|
||||
}
|
||||
|
||||
export type ClassProgressSubmitFormData = {
|
||||
subject_sections?: Record<string, { label?: string; db_subject?: string }>
|
||||
status_options?: Record<string, string>
|
||||
sunday_options?: string[]
|
||||
curriculum?: Record<string, CurriculumRow[]>
|
||||
classes?: Array<{
|
||||
class_section_id?: number
|
||||
class_section_name?: string
|
||||
class_id?: number | null
|
||||
class_name?: string | null
|
||||
}>
|
||||
defaults?: {
|
||||
class_id?: number | null
|
||||
class_section_id?: number | null
|
||||
school_year?: string | null
|
||||
semester?: string | null
|
||||
week_start?: string | null
|
||||
}
|
||||
}
|
||||
|
||||
type UnitRow = {
|
||||
unitNumber: number | 'custom' | null
|
||||
chapter: string
|
||||
}
|
||||
|
||||
function unwrapSubmitForm(raw: unknown): ClassProgressSubmitFormData | null {
|
||||
if (raw == null || typeof raw !== 'object') return null
|
||||
const o = raw as Record<string, unknown>
|
||||
const inner = o.data
|
||||
if (inner != null && typeof inner === 'object' && !Array.isArray(inner)) {
|
||||
return inner as ClassProgressSubmitFormData
|
||||
}
|
||||
return raw as ClassProgressSubmitFormData
|
||||
}
|
||||
|
||||
function formatWeekLabel(iso: string): string {
|
||||
const m = /^(\d{4})-(\d{2})-(\d{2})/.exec(iso)
|
||||
if (!m) return iso
|
||||
return `${m[2]}/${m[3]}/${m[1]}`
|
||||
}
|
||||
|
||||
function unitChoices(curriculum: CurriculumRow[]): Array<{ value: number; label: string }> {
|
||||
const seen = new Map<number, string>()
|
||||
for (const r of curriculum) {
|
||||
const n = Number(r.unit_number ?? 0)
|
||||
if (!Number.isFinite(n) || n <= 0) continue
|
||||
if (!seen.has(n)) {
|
||||
const t = String(r.unit_title ?? '').trim()
|
||||
seen.set(n, t || `Unit ${n}`)
|
||||
}
|
||||
}
|
||||
return [...seen.entries()]
|
||||
.sort((a, b) => a[0] - b[0])
|
||||
.map(([value, label]) => ({ value, label }))
|
||||
}
|
||||
|
||||
function chaptersForUnit(curriculum: CurriculumRow[], unitNumber: number): string[] {
|
||||
const out: string[] = []
|
||||
const seen = new Set<string>()
|
||||
for (const r of curriculum) {
|
||||
if (Number(r.unit_number ?? 0) !== unitNumber) continue
|
||||
const ch = String(r.chapter_name ?? '').trim()
|
||||
if (ch && !seen.has(ch)) {
|
||||
seen.add(ch)
|
||||
out.push(ch)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
function unitLabelForPayload(curriculum: CurriculumRow[], unitNumber: number): string {
|
||||
for (const r of curriculum) {
|
||||
if (Number(r.unit_number ?? 0) === unitNumber) {
|
||||
const t = String(r.unit_title ?? '').trim()
|
||||
if (t) return t
|
||||
}
|
||||
}
|
||||
return `Unit ${unitNumber}`
|
||||
}
|
||||
|
||||
function islamicSelectionValid(rows: UnitRow[]): boolean {
|
||||
for (const r of rows) {
|
||||
const ch = r.chapter.trim()
|
||||
if (r.unitNumber === 'custom' && ch !== '') return true
|
||||
if (typeof r.unitNumber === 'number' && r.unitNumber > 0 && ch !== '') return true
|
||||
if (typeof r.unitNumber === 'number' && r.unitNumber > 0 && ch === '') continue
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function appendArray(fd: FormData, key: string, values: string[]) {
|
||||
values.forEach((v, i) => {
|
||||
fd.append(`${key}[${i}]`, v)
|
||||
})
|
||||
}
|
||||
|
||||
/** `teacher/class_progress_submit.php` — weekly progress form; POST `/api/v1/teacher/class-progress-submit`. */
|
||||
export function TeacherClassProgressSubmitPage() {
|
||||
const [search] = useSearchParams()
|
||||
const classIdQ = search.get('class_id')
|
||||
const path = useMemo(() => {
|
||||
const params = new URLSearchParams()
|
||||
if (classIdQ) params.set('class_id', classIdQ)
|
||||
const q = params.toString()
|
||||
return `/class-progress-submit${q ? `?${q}` : ''}`
|
||||
}, [classIdQ])
|
||||
|
||||
const { data: raw, loading, error, reload } = useTeacherResource<unknown>(path)
|
||||
const data = useMemo(() => unwrapSubmitForm(raw), [raw])
|
||||
|
||||
const subjectSections = data?.subject_sections ?? {}
|
||||
const slugs = useMemo(() => Object.keys(subjectSections), [subjectSections])
|
||||
const sundayOptions = data?.sunday_options ?? []
|
||||
const classes = data?.classes ?? []
|
||||
const defaults = data?.defaults ?? {}
|
||||
const curriculumIslamic = data?.curriculum?.islamic ?? []
|
||||
const curriculumQuran = data?.curriculum?.quran ?? []
|
||||
|
||||
const unitPickList = useMemo(() => unitChoices(curriculumIslamic), [curriculumIslamic])
|
||||
|
||||
const classIds = useMemo(() => {
|
||||
const s = new Set<number>()
|
||||
for (const c of classes) {
|
||||
const id = Number(c.class_id ?? 0)
|
||||
if (id > 0) s.add(id)
|
||||
}
|
||||
return [...s].sort((a, b) => a - b)
|
||||
}, [classes])
|
||||
|
||||
const [weekStart, setWeekStart] = useState('')
|
||||
const [classSectionId, setClassSectionId] = useState<number | ''>('')
|
||||
const [covered, setCovered] = useState<Record<string, string>>({})
|
||||
const [homework, setHomework] = useState<Record<string, string>>({})
|
||||
const [islamicRows, setIslamicRows] = useState<UnitRow[]>([{ unitNumber: null, chapter: '' }])
|
||||
const [quranRows, setQuranRows] = useState<UnitRow[]>([{ unitNumber: null, chapter: '' }])
|
||||
const [attachmentIslamic, setAttachmentIslamic] = useState<File[]>([])
|
||||
const [attachmentQuran, setAttachmentQuran] = useState<File[]>([])
|
||||
const [confirmOverwrite, setConfirmOverwrite] = useState(false)
|
||||
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
const [submitError, setSubmitError] = useState<string | null>(null)
|
||||
const [submitOk, setSubmitOk] = useState<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (!data) return
|
||||
const ws = defaults.week_start ?? sundayOptions[0] ?? ''
|
||||
setWeekStart(ws)
|
||||
const sid = Number(defaults.class_section_id ?? 0)
|
||||
setClassSectionId(sid > 0 ? sid : '')
|
||||
setCovered({})
|
||||
setHomework({})
|
||||
setIslamicRows([{ unitNumber: null, chapter: '' }])
|
||||
setQuranRows([{ unitNumber: null, chapter: '' }])
|
||||
setAttachmentIslamic([])
|
||||
setAttachmentQuran([])
|
||||
setConfirmOverwrite(false)
|
||||
setSubmitError(null)
|
||||
setSubmitOk(null)
|
||||
}, [data, defaults.class_section_id, defaults.week_start, sundayOptions])
|
||||
|
||||
const resolvedClassId = useMemo(() => {
|
||||
const fromQ = classIdQ ? Number(classIdQ) : 0
|
||||
if (fromQ > 0) return fromQ
|
||||
return Number(defaults.class_id ?? 0)
|
||||
}, [classIdQ, defaults.class_id])
|
||||
|
||||
const sectionsForClass = useMemo(() => {
|
||||
if (!resolvedClassId) return classes
|
||||
return classes.filter((c) => Number(c.class_id ?? 0) === resolvedClassId)
|
||||
}, [classes, resolvedClassId])
|
||||
|
||||
useEffect(() => {
|
||||
if (classSectionId === '' && sectionsForClass.length === 1) {
|
||||
const only = Number(sectionsForClass[0].class_section_id ?? 0)
|
||||
if (only > 0) setClassSectionId(only)
|
||||
}
|
||||
}, [sectionsForClass, classSectionId])
|
||||
|
||||
const canSubmit = useMemo(() => {
|
||||
if (!weekStart || !classSectionId) return false
|
||||
if (!islamicSelectionValid(islamicRows)) return false
|
||||
const covIslamic = (covered.islamic ?? '').trim()
|
||||
if (covIslamic === '') return false
|
||||
return true
|
||||
}, [weekStart, classSectionId, islamicRows, covered])
|
||||
|
||||
const updateIslamicRow = useCallback((idx: number, patch: Partial<UnitRow>) => {
|
||||
setIslamicRows((prev) => prev.map((r, i) => (i === idx ? { ...r, ...patch } : r)))
|
||||
}, [])
|
||||
|
||||
const updateQuranRow = useCallback((idx: number, patch: Partial<UnitRow>) => {
|
||||
setQuranRows((prev) => prev.map((r, i) => (i === idx ? { ...r, ...patch } : r)))
|
||||
}, [])
|
||||
|
||||
const handleSubmit = useCallback(async () => {
|
||||
setSubmitError(null)
|
||||
setSubmitOk(null)
|
||||
if (!canSubmit || classSectionId === '') return
|
||||
|
||||
const unitIslamic: string[] = []
|
||||
const chapterIslamic: string[] = []
|
||||
for (const r of islamicRows) {
|
||||
const ch = r.chapter.trim()
|
||||
if (r.unitNumber === 'custom') {
|
||||
if (ch === '') continue
|
||||
unitIslamic.push(CUSTOM_UNIT)
|
||||
chapterIslamic.push(ch)
|
||||
} else if (typeof r.unitNumber === 'number' && r.unitNumber > 0) {
|
||||
if (ch === '') continue
|
||||
unitIslamic.push(unitLabelForPayload(curriculumIslamic, r.unitNumber))
|
||||
chapterIslamic.push(ch)
|
||||
}
|
||||
}
|
||||
|
||||
const unitQuran: string[] = []
|
||||
const chapterQuran: string[] = []
|
||||
for (const r of quranRows) {
|
||||
const ch = r.chapter.trim()
|
||||
if (r.unitNumber === 'custom' && ch !== '') {
|
||||
unitQuran.push(CUSTOM_UNIT)
|
||||
chapterQuran.push(ch)
|
||||
} else if (typeof r.unitNumber === 'number' && r.unitNumber > 0 && ch !== '') {
|
||||
unitQuran.push(unitLabelForPayload(curriculumQuran, r.unitNumber))
|
||||
chapterQuran.push(ch)
|
||||
}
|
||||
}
|
||||
|
||||
const fd = new FormData()
|
||||
fd.append('class_section_id', String(classSectionId))
|
||||
fd.append('week_start', weekStart)
|
||||
if (confirmOverwrite) fd.append('confirm_overwrite', '1')
|
||||
|
||||
for (const slug of slugs) {
|
||||
const cov = (covered[slug] ?? '').trim()
|
||||
const hw = (homework[slug] ?? '').trim()
|
||||
fd.append(`covered_${slug}`, cov)
|
||||
if (hw) fd.append(`homework_${slug}`, hw)
|
||||
}
|
||||
|
||||
appendArray(fd, 'unit_islamic', unitIslamic)
|
||||
appendArray(fd, 'chapter_islamic', chapterIslamic)
|
||||
appendArray(fd, 'unit_quran', unitQuran)
|
||||
appendArray(fd, 'chapter_quran', chapterQuran)
|
||||
|
||||
attachmentIslamic.forEach((f) => fd.append('attachment_islamic[]', f))
|
||||
attachmentQuran.forEach((f) => fd.append('attachment_quran[]', f))
|
||||
|
||||
setSubmitting(true)
|
||||
try {
|
||||
const res = await apiFetch<{ status?: boolean; message?: string }>('/api/v1/teacher/class-progress-submit', {
|
||||
method: 'POST',
|
||||
body: fd,
|
||||
})
|
||||
setSubmitOk(typeof res?.message === 'string' ? res.message : 'Progress saved.')
|
||||
setConfirmOverwrite(false)
|
||||
reload()
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof ApiHttpError && e.status === 409) {
|
||||
setSubmitError(
|
||||
'A report already exists for this week. Check “Replace existing week” and submit again to overwrite.',
|
||||
)
|
||||
return
|
||||
}
|
||||
const msg = e instanceof ApiHttpError ? e.message : 'Unable to save progress.'
|
||||
setSubmitError(msg)
|
||||
} finally {
|
||||
setSubmitting(false)
|
||||
}
|
||||
}, [
|
||||
canSubmit,
|
||||
classSectionId,
|
||||
weekStart,
|
||||
confirmOverwrite,
|
||||
slugs,
|
||||
covered,
|
||||
homework,
|
||||
islamicRows,
|
||||
quranRows,
|
||||
curriculumIslamic,
|
||||
curriculumQuran,
|
||||
attachmentIslamic,
|
||||
attachmentQuran,
|
||||
reload,
|
||||
])
|
||||
|
||||
const subtitleParts = [defaults.school_year, defaults.semester].filter(Boolean).join(' · ')
|
||||
|
||||
return (
|
||||
<TeacherShell
|
||||
title="Submit class progress"
|
||||
subtitle={subtitleParts || undefined}
|
||||
loading={loading}
|
||||
error={error}
|
||||
>
|
||||
{!loading && raw != null && data == null ? (
|
||||
<div className="alert alert-warning">
|
||||
Unexpected response shape.
|
||||
<pre className="small mb-0 mt-2 bg-light p-2 rounded overflow-auto" style={{ maxHeight: 200 }}>
|
||||
{JSON.stringify(raw, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{data ? (
|
||||
<div className="d-flex flex-column gap-4">
|
||||
<div className="d-flex flex-wrap justify-content-end gap-2">
|
||||
<Link to="/app/teacher/class-progress-history" className="btn btn-outline-primary btn-sm">
|
||||
Past submissions
|
||||
</Link>
|
||||
</div>
|
||||
{classes.length === 0 ? (
|
||||
<div className="alert alert-info mb-0">
|
||||
You have no class assignments for the current term, so progress cannot be submitted.
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{classes.length > 0 && classIds.length > 1 ? (
|
||||
<div className="d-flex flex-wrap align-items-center gap-2">
|
||||
<span className="small text-muted me-1">Class:</span>
|
||||
{classIds.map((id) => {
|
||||
const label =
|
||||
classes.find((c) => Number(c.class_id) === id)?.class_name?.trim() || `Class ${id}`
|
||||
const active = resolvedClassId === id
|
||||
return (
|
||||
<Link
|
||||
key={id}
|
||||
to={`/app/teacher/class-progress-submit?class_id=${encodeURIComponent(String(id))}`}
|
||||
className={`btn btn-sm ${active ? 'btn-primary' : 'btn-outline-primary'}`}
|
||||
>
|
||||
{label}
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{submitOk ? (
|
||||
<div className="alert alert-success py-2 mb-0" role="status">
|
||||
{submitOk}
|
||||
</div>
|
||||
) : null}
|
||||
{submitError ? (
|
||||
<div className="alert alert-danger py-2 mb-0" role="alert">
|
||||
{submitError}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{classes.length > 0 ? (
|
||||
<>
|
||||
<section className="border rounded p-3 bg-white">
|
||||
<h2 className="h6 mb-3">Week & section</h2>
|
||||
<div className="row g-3">
|
||||
<div className="col-md-6">
|
||||
<label className="form-label" htmlFor="cp-week-start">
|
||||
Week (Sunday start)
|
||||
</label>
|
||||
<select
|
||||
id="cp-week-start"
|
||||
className="form-select form-select-sm"
|
||||
value={weekStart}
|
||||
onChange={(e) => setWeekStart(e.target.value)}
|
||||
>
|
||||
{sundayOptions.length === 0 ? <option value="">No dates configured</option> : null}
|
||||
{sundayOptions.map((d) => (
|
||||
<option key={d} value={d}>
|
||||
{formatWeekLabel(d)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="col-md-6">
|
||||
<label className="form-label" htmlFor="cp-section">
|
||||
Class section
|
||||
</label>
|
||||
<select
|
||||
id="cp-section"
|
||||
className="form-select form-select-sm"
|
||||
value={classSectionId === '' ? '' : String(classSectionId)}
|
||||
onChange={(e) => setClassSectionId(e.target.value ? Number(e.target.value) : '')}
|
||||
>
|
||||
<option value="">Select section…</option>
|
||||
{sectionsForClass.map((c) => {
|
||||
const id = Number(c.class_section_id ?? 0)
|
||||
if (!id) return null
|
||||
const name = String(c.class_section_name ?? '').trim() || `Section ${id}`
|
||||
return (
|
||||
<option key={id} value={id}>
|
||||
{name}
|
||||
</option>
|
||||
)
|
||||
})}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="border rounded p-3 bg-white">
|
||||
<h2 className="h6 mb-1">Islamic Studies — units / chapters</h2>
|
||||
<p className="small text-muted mb-3">
|
||||
Select at least one unit/chapter pair (or Custom + description). Required before submit.
|
||||
</p>
|
||||
<div className="d-flex flex-column gap-2">
|
||||
{islamicRows.map((row, idx) => (
|
||||
<div key={`is-${idx}`} className="row g-2 align-items-end">
|
||||
<div className="col-md-5">
|
||||
<label className="form-label small mb-0">Unit</label>
|
||||
<select
|
||||
className="form-select form-select-sm"
|
||||
value={
|
||||
row.unitNumber === 'custom'
|
||||
? 'custom'
|
||||
: row.unitNumber != null
|
||||
? String(row.unitNumber)
|
||||
: ''
|
||||
}
|
||||
onChange={(e) => {
|
||||
const v = e.target.value
|
||||
if (v === '') updateIslamicRow(idx, { unitNumber: null, chapter: '' })
|
||||
else if (v === 'custom') updateIslamicRow(idx, { unitNumber: 'custom', chapter: '' })
|
||||
else updateIslamicRow(idx, { unitNumber: Number(v), chapter: '' })
|
||||
}}
|
||||
>
|
||||
<option value="">—</option>
|
||||
{unitPickList.map((u) => (
|
||||
<option key={u.value} value={u.value}>
|
||||
{u.label}
|
||||
</option>
|
||||
))}
|
||||
<option value="custom">{CUSTOM_UNIT}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="col-md-5">
|
||||
<label className="form-label small mb-0">Chapter / detail</label>
|
||||
{row.unitNumber != null &&
|
||||
row.unitNumber !== 'custom' &&
|
||||
typeof row.unitNumber === 'number' &&
|
||||
chaptersForUnit(curriculumIslamic, row.unitNumber).length > 0 ? (
|
||||
<select
|
||||
className="form-select form-select-sm"
|
||||
value={row.chapter}
|
||||
onChange={(e) => updateIslamicRow(idx, { chapter: e.target.value })}
|
||||
>
|
||||
<option value="">—</option>
|
||||
{chaptersForUnit(curriculumIslamic, row.unitNumber).map((ch) => (
|
||||
<option key={ch} value={ch}>
|
||||
{ch}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
) : (
|
||||
<input
|
||||
type="text"
|
||||
className="form-control form-control-sm"
|
||||
value={row.chapter}
|
||||
onChange={(e) => updateIslamicRow(idx, { chapter: e.target.value })}
|
||||
placeholder={row.unitNumber === 'custom' ? 'Describe custom topic…' : 'Chapter or detail'}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="col-md-2 d-flex gap-1">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-outline-secondary btn-sm"
|
||||
onClick={() =>
|
||||
setIslamicRows((prev) => [...prev.slice(0, idx + 1), { unitNumber: null, chapter: '' }, ...prev.slice(idx + 1)])
|
||||
}
|
||||
>
|
||||
+ Row
|
||||
</button>
|
||||
{islamicRows.length > 1 ? (
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-outline-danger btn-sm"
|
||||
onClick={() => setIslamicRows((prev) => prev.filter((_, i) => i !== idx))}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{slugs.map((slug) => {
|
||||
const label = subjectSections[slug]?.label ?? slug
|
||||
const attKey = slug === 'islamic' ? attachmentIslamic : attachmentQuran
|
||||
const setAtt = slug === 'islamic' ? setAttachmentIslamic : setAttachmentQuran
|
||||
return (
|
||||
<Fragment key={slug}>
|
||||
{slug === 'quran' && curriculumQuran.length > 0 ? (
|
||||
<section className="border rounded p-3 bg-white mb-3">
|
||||
<h2 className="h6 mb-1">Quran / Arabic — units / chapters (optional)</h2>
|
||||
<p className="small text-muted mb-3">Optional rows when curriculum is available.</p>
|
||||
<div className="d-flex flex-column gap-2">
|
||||
{quranRows.map((row, idx) => {
|
||||
const qUnits = unitChoices(curriculumQuran)
|
||||
return (
|
||||
<div key={`qr-${idx}`} className="row g-2 align-items-end">
|
||||
<div className="col-md-5">
|
||||
<label className="form-label small mb-0">Unit</label>
|
||||
<select
|
||||
className="form-select form-select-sm"
|
||||
value={
|
||||
row.unitNumber === 'custom'
|
||||
? 'custom'
|
||||
: row.unitNumber != null
|
||||
? String(row.unitNumber)
|
||||
: ''
|
||||
}
|
||||
onChange={(e) => {
|
||||
const v = e.target.value
|
||||
if (v === '') updateQuranRow(idx, { unitNumber: null, chapter: '' })
|
||||
else if (v === 'custom') updateQuranRow(idx, { unitNumber: 'custom', chapter: '' })
|
||||
else updateQuranRow(idx, { unitNumber: Number(v), chapter: '' })
|
||||
}}
|
||||
>
|
||||
<option value="">—</option>
|
||||
{qUnits.map((u) => (
|
||||
<option key={u.value} value={u.value}>
|
||||
{u.label}
|
||||
</option>
|
||||
))}
|
||||
<option value="custom">{CUSTOM_UNIT}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="col-md-5">
|
||||
<label className="form-label small mb-0">Chapter / detail</label>
|
||||
{row.unitNumber != null &&
|
||||
row.unitNumber !== 'custom' &&
|
||||
typeof row.unitNumber === 'number' &&
|
||||
chaptersForUnit(curriculumQuran, row.unitNumber).length > 0 ? (
|
||||
<select
|
||||
className="form-select form-select-sm"
|
||||
value={row.chapter}
|
||||
onChange={(e) => updateQuranRow(idx, { chapter: e.target.value })}
|
||||
>
|
||||
<option value="">—</option>
|
||||
{chaptersForUnit(curriculumQuran, row.unitNumber).map((ch) => (
|
||||
<option key={ch} value={ch}>
|
||||
{ch}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
) : (
|
||||
<input
|
||||
type="text"
|
||||
className="form-control form-control-sm"
|
||||
value={row.chapter}
|
||||
onChange={(e) => updateQuranRow(idx, { chapter: e.target.value })}
|
||||
placeholder="Chapter or custom text…"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="col-md-2 d-flex gap-1">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-outline-secondary btn-sm"
|
||||
onClick={() =>
|
||||
setQuranRows((prev) => [
|
||||
...prev.slice(0, idx + 1),
|
||||
{ unitNumber: null, chapter: '' },
|
||||
...prev.slice(idx + 1),
|
||||
])
|
||||
}
|
||||
>
|
||||
+ Row
|
||||
</button>
|
||||
{quranRows.length > 1 ? (
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-outline-danger btn-sm"
|
||||
onClick={() => setQuranRows((prev) => prev.filter((_, i) => i !== idx))}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
) : null}
|
||||
<section className="border rounded p-3 bg-white">
|
||||
<h2 className="h6 mb-3">{label}</h2>
|
||||
<div className="mb-2">
|
||||
<label className="form-label small" htmlFor={`covered-${slug}`}>
|
||||
Material covered <span className="text-danger">{slug === 'islamic' ? '*' : ''}</span>
|
||||
</label>
|
||||
<textarea
|
||||
id={`covered-${slug}`}
|
||||
className="form-control form-control-sm"
|
||||
rows={3}
|
||||
value={covered[slug] ?? ''}
|
||||
onChange={(e) => setCovered((p) => ({ ...p, [slug]: e.target.value }))}
|
||||
placeholder="Topics taught this week…"
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-2">
|
||||
<label className="form-label small" htmlFor={`hw-${slug}`}>
|
||||
Homework
|
||||
</label>
|
||||
<textarea
|
||||
id={`hw-${slug}`}
|
||||
className="form-control form-control-sm"
|
||||
rows={2}
|
||||
value={homework[slug] ?? ''}
|
||||
onChange={(e) => setHomework((p) => ({ ...p, [slug]: e.target.value }))}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="form-label small" htmlFor={`att-${slug}`}>
|
||||
Attachment (PDF or image)
|
||||
</label>
|
||||
<input
|
||||
id={`att-${slug}`}
|
||||
type="file"
|
||||
className="form-control form-control-sm"
|
||||
accept=".pdf,.png,.jpg,.jpeg,image/*,application/pdf"
|
||||
multiple
|
||||
onChange={(e) => setAtt(Array.from(e.target.files ?? []))}
|
||||
/>
|
||||
{attKey.length > 0 ? (
|
||||
<div className="small text-muted mt-1">{attKey.map((f) => f.name).join(', ')}</div>
|
||||
) : null}
|
||||
</div>
|
||||
</section>
|
||||
</Fragment>
|
||||
)
|
||||
})}
|
||||
|
||||
<div className="form-check mb-2">
|
||||
<input
|
||||
id="cp-overwrite"
|
||||
type="checkbox"
|
||||
className="form-check-input"
|
||||
checked={confirmOverwrite}
|
||||
onChange={(e) => setConfirmOverwrite(e.target.checked)}
|
||||
/>
|
||||
<label className="form-check-label" htmlFor="cp-overwrite">
|
||||
Replace existing progress for this week (overwrite)
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="d-flex flex-wrap gap-2 align-items-center">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-primary"
|
||||
disabled={!canSubmit || submitting}
|
||||
onClick={() => void handleSubmit()}
|
||||
>
|
||||
{submitting ? 'Saving…' : 'Submit progress'}
|
||||
</button>
|
||||
<button type="button" className="btn btn-outline-secondary btn-sm" onClick={() => reload()}>
|
||||
Reload form
|
||||
</button>
|
||||
{!canSubmit ? (
|
||||
<span className="small text-muted">
|
||||
Choose section, week, Islamic unit/chapter, and material covered for Islamic Studies.
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
</TeacherShell>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user