fix teacher, parent and admin pages
This commit is contained in:
@@ -0,0 +1,198 @@
|
||||
import { type FormEvent, useEffect, useMemo, useState } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { createEarlyDismissal, fetchEarlyDismissalStudentOptions } from '../../api/session'
|
||||
import { EARLY_DISMISSALS_PATH } from './attendancePaths'
|
||||
|
||||
type StudentOpt = {
|
||||
id: number
|
||||
firstname: string
|
||||
lastname: string
|
||||
section?: string | null
|
||||
}
|
||||
|
||||
/** Port of `Views/attendance/early_dismissals_add.php` */
|
||||
export function EarlyDismissalsAddPage() {
|
||||
const [students, setStudents] = useState<StudentOpt[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [msg, setMsg] = useState<string | null>(null)
|
||||
|
||||
const [query, setQuery] = useState('')
|
||||
const [studentId, setStudentId] = useState<number | null>(null)
|
||||
const [label, setLabel] = useState('')
|
||||
const [date, setDate] = useState(() => new Date().toISOString().slice(0, 10))
|
||||
const [dismissTime, setDismissTime] = useState('')
|
||||
const [reason, setReason] = useState('')
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
;(async () => {
|
||||
try {
|
||||
const data = await fetchEarlyDismissalStudentOptions()
|
||||
if (cancelled) return
|
||||
const raw = data.students ?? []
|
||||
setStudents(
|
||||
raw
|
||||
.map((s) => ({
|
||||
id: Number(s.id ?? 0),
|
||||
firstname: String(s.firstname ?? ''),
|
||||
lastname: String(s.lastname ?? ''),
|
||||
section: s.class_section_name ?? null,
|
||||
}))
|
||||
.filter((s) => s.id > 0),
|
||||
)
|
||||
} catch (e) {
|
||||
if (!cancelled) setError(e instanceof Error ? e.message : 'Unable to load students.')
|
||||
} finally {
|
||||
if (!cancelled) setLoading(false)
|
||||
}
|
||||
})()
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [])
|
||||
|
||||
const enriched = useMemo(() => {
|
||||
const norm = (s: string) => s.toLowerCase().trim()
|
||||
return students.map((s) => {
|
||||
const display = `${s.firstname} ${s.lastname}${s.section ? ` — ${s.section}` : ''}`
|
||||
const key = `${norm(s.firstname)} ${norm(s.lastname)} ${norm(s.section ?? '')}`
|
||||
return { ...s, display, key }
|
||||
})
|
||||
}, [students])
|
||||
|
||||
const matches = useMemo(() => {
|
||||
const q = query.toLowerCase().trim()
|
||||
if (!q) return []
|
||||
return enriched.filter((s) => s.key.includes(q)).slice(0, 20)
|
||||
}, [enriched, query])
|
||||
|
||||
async function onSubmit(e: FormEvent) {
|
||||
e.preventDefault()
|
||||
setError(null)
|
||||
setMsg(null)
|
||||
if (!studentId) {
|
||||
setError('Please select a student from the list.')
|
||||
return
|
||||
}
|
||||
try {
|
||||
await createEarlyDismissal({
|
||||
student_id: studentId,
|
||||
date,
|
||||
dismiss_time: dismissTime,
|
||||
reason: reason || null,
|
||||
})
|
||||
setMsg('Early dismissal saved.')
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Save failed.')
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container-xxl py-4">
|
||||
<div className="d-flex align-items-center justify-content-between flex-wrap gap-2 mb-3">
|
||||
<h2 className="mb-0">Add Early Dismissal</h2>
|
||||
<Link to={EARLY_DISMISSALS_PATH} className="btn btn-outline-secondary">
|
||||
Back to List
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{error ? <div className="alert alert-danger">{error}</div> : null}
|
||||
{msg ? <div className="alert alert-success">{msg}</div> : null}
|
||||
|
||||
<div className="card shadow-sm">
|
||||
<div className="card-body">
|
||||
<form onSubmit={onSubmit}>
|
||||
<div className="row gy-3">
|
||||
<div className="col-md-5 position-relative">
|
||||
<label className="form-label">Student</label>
|
||||
<input
|
||||
type="text"
|
||||
className={`form-control${error && !studentId ? ' is-invalid' : ''}`}
|
||||
autoComplete="off"
|
||||
placeholder="Type student name to search…"
|
||||
value={label}
|
||||
onChange={(ev) => {
|
||||
setLabel(ev.target.value)
|
||||
setStudentId(null)
|
||||
setQuery(ev.target.value)
|
||||
}}
|
||||
/>
|
||||
{query.trim() && matches.length > 0 ? (
|
||||
<div
|
||||
className="list-group position-absolute w-100 shadow-sm"
|
||||
style={{ zIndex: 1000, maxHeight: 240, overflow: 'auto' }}
|
||||
>
|
||||
{matches.map((m) => (
|
||||
<button
|
||||
key={m.id}
|
||||
type="button"
|
||||
className="list-group-item list-group-item-action"
|
||||
onClick={() => {
|
||||
setStudentId(m.id)
|
||||
setLabel(m.display)
|
||||
setQuery('')
|
||||
}}
|
||||
>
|
||||
{m.display}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
<div className="form-text">Start typing first or last name, then pick from the list.</div>
|
||||
</div>
|
||||
|
||||
<div className="col-md-3">
|
||||
<label className="form-label">Date (Sunday)</label>
|
||||
<input
|
||||
type="date"
|
||||
className="form-control"
|
||||
required
|
||||
value={date}
|
||||
onChange={(ev) => setDate(ev.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="col-md-2">
|
||||
<label className="form-label">Dismissal Time</label>
|
||||
<input
|
||||
type="time"
|
||||
className="form-control"
|
||||
required
|
||||
value={dismissTime}
|
||||
onChange={(ev) => setDismissTime(ev.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="col-12">
|
||||
<label className="form-label">Reason (optional)</label>
|
||||
<textarea
|
||||
name="reason"
|
||||
className="form-control"
|
||||
rows={2}
|
||||
placeholder="e.g., Family appointment"
|
||||
value={reason}
|
||||
onChange={(ev) => setReason(ev.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="col-12 d-flex gap-2">
|
||||
<button type="submit" className="btn btn-primary" disabled={loading}>
|
||||
Save Early Dismissal
|
||||
</button>
|
||||
<Link to={EARLY_DISMISSALS_PATH} className="btn btn-outline-secondary">
|
||||
Cancel
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-3 small text-muted">
|
||||
Note: Early dismissals do not change daily present/absent status; they are shown for staff awareness and
|
||||
pickup coordination.
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user