add parent pages

This commit is contained in:
root
2026-04-23 02:24:05 -04:00
parent 9191fd32f0
commit 94700d4f0f
30 changed files with 8179 additions and 634 deletions
+159 -50
View File
@@ -4,12 +4,19 @@ import {
fetchAttendanceReportForm,
fetchParentEnrollments,
fetchParentEventsOverview,
fetchParentInvoices,
fetchParentProfile,
fetchParentRegistrationOverview,
submitContactMessage,
} from '../../api/session'
import type { ParentEnrollmentStudent, ParentInvoiceRow } from '../../api/types'
fetchParentInvoices,
fetchParentProfile,
fetchParentRegistrationOverview,
submitParentAttendanceReport,
submitContactMessage,
} from '../../api/session'
import type {
ParentAttendanceReportFormResponse,
ParentEnrollmentStudent,
ParentEventsOverviewResponse,
ParentInvoiceRow,
ParentRegistrationOverviewResponse,
} from '../../api/types'
import { useAuth } from '../../auth/AuthProvider'
/** `Views/parent/contact.php` — POST /api/v1/contact */
@@ -300,19 +307,19 @@ export function ParentPaymentInvoicesPage() {
}
/** `Views/parent/event_participation.php` — GET/POST /api/v1/parents/events … */
export function ParentEventsPage() {
const [error, setError] = useState<string | null>(null)
const [loading, setLoading] = useState(true)
const [summary, setSummary] = useState<string>('')
export function ParentEventsPage() {
const [error, setError] = useState<string | null>(null)
const [loading, setLoading] = useState(true)
const [data, setData] = useState<ParentEventsOverviewResponse | null>(null)
useEffect(() => {
let cancelled = false
;(async () => {
setLoading(true)
try {
const data = await fetchParentEventsOverview()
if (cancelled) return
setSummary(JSON.stringify(data, null, 2))
const data = await fetchParentEventsOverview()
if (cancelled) return
setData(data)
setError(null)
} catch (e) {
if (!cancelled) setError(e instanceof Error ? e.message : 'Failed to load events.')
@@ -335,28 +342,43 @@ export function ParentEventsPage() {
{loading ? (
<p className="text-muted small">Loading</p>
) : (
<pre className="small bg-light p-2 border rounded" style={{ maxHeight: 320, overflow: 'auto' }}>
{summary}
</pre>
)}
<div className="table-responsive">
<table className="table table-sm table-striped align-middle">
<thead>
<tr><th>Event</th><th>Date</th><th>Description</th></tr>
</thead>
<tbody>
{(data?.activeEvents ?? []).length === 0 ? (
<tr><td colSpan={3} className="text-muted">No active events.</td></tr>
) : (data?.activeEvents ?? []).map((event) => (
<tr key={String(event.id ?? event.title ?? event.event_name)}>
<td>{event.title ?? event.event_name ?? '—'}</td>
<td>{event.event_date ?? event.start_date ?? '—'}</td>
<td>{event.description ?? '—'}</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</ParentParityShell>
)
}
/** `Views/parent/register_student.php` — GET /api/v1/parents/registration */
export function ParentRegisterStudentPage() {
const [error, setError] = useState<string | null>(null)
const [loading, setLoading] = useState(true)
const [summary, setSummary] = useState<string>('')
export function ParentRegisterStudentPage() {
const [error, setError] = useState<string | null>(null)
const [loading, setLoading] = useState(true)
const [data, setData] = useState<ParentRegistrationOverviewResponse | null>(null)
useEffect(() => {
let cancelled = false
;(async () => {
setLoading(true)
try {
const data = await fetchParentRegistrationOverview()
if (cancelled) return
setSummary(JSON.stringify(data, null, 2))
const data = await fetchParentRegistrationOverview()
if (cancelled) return
setData(data)
setError(null)
} catch (e) {
if (!cancelled) setError(e instanceof Error ? e.message : 'Failed to load registration.')
@@ -379,28 +401,68 @@ export function ParentRegisterStudentPage() {
{loading ? (
<p className="text-muted small">Loading</p>
) : (
<pre className="small bg-light p-2 border rounded" style={{ maxHeight: 360, overflow: 'auto' }}>
{summary}
</pre>
)}
<>
<div className="d-flex flex-wrap gap-2 mb-3">
<a className="btn btn-success btn-sm" href="/app/parent/add-student-form">Add student</a>
<a className="btn btn-outline-success btn-sm" href="/app/parent/add-emergency-form">Add emergency contact</a>
</div>
<h3 className="h6">Students</h3>
<div className="table-responsive mb-3">
<table className="table table-sm table-striped">
<thead><tr><th>Name</th><th>Grade</th><th>Class</th><th>Status</th></tr></thead>
<tbody>
{(data?.existingKids ?? []).length === 0 ? (
<tr><td colSpan={4} className="text-muted">No students registered.</td></tr>
) : (data?.existingKids ?? []).map((student) => (
<tr key={student.id}>
<td>{student.firstname} {student.lastname}</td>
<td>{student.registration_grade ?? '—'}</td>
<td>{student.class_section ?? '—'}</td>
<td>{student.admission_status ?? student.enrollment_status ?? '—'}</td>
</tr>
))}
</tbody>
</table>
</div>
<h3 className="h6">Emergency contacts</h3>
<div className="table-responsive">
<table className="table table-sm table-striped">
<thead><tr><th>Name</th><th>Phone</th><th>Email</th><th>Relation</th></tr></thead>
<tbody>
{(data?.emergencies ?? []).length === 0 ? (
<tr><td colSpan={4} className="text-muted">No emergency contacts.</td></tr>
) : (data?.emergencies ?? []).map((contact) => (
<tr key={contact.id}>
<td>{contact.name}</td>
<td>{contact.cellphone}</td>
<td>{contact.email ?? '—'}</td>
<td>{contact.relation ?? '—'}</td>
</tr>
))}
</tbody>
</table>
</div>
</>
)}
</ParentParityShell>
)
}
/** `Views/parent/report_attendance.php` — form: GET /api/v1/parents/attendance-reports/form */
export function ParentReportAttendancePage() {
const [error, setError] = useState<string | null>(null)
const [loading, setLoading] = useState(true)
const [summary, setSummary] = useState<string>('')
export function ParentReportAttendancePage() {
const [error, setError] = useState<string | null>(null)
const [status, setStatus] = useState<string | null>(null)
const [loading, setLoading] = useState(true)
const [data, setData] = useState<ParentAttendanceReportFormResponse | null>(null)
useEffect(() => {
let cancelled = false
;(async () => {
setLoading(true)
try {
const data = await fetchAttendanceReportForm()
if (cancelled) return
setSummary(JSON.stringify(data, null, 2))
const data = await fetchAttendanceReportForm()
if (cancelled) return
setData(data)
setError(null)
} catch (e) {
if (!cancelled)
@@ -420,18 +482,65 @@ export function ParentReportAttendancePage() {
ciViewFile="report_attendance.php"
legacyCiRoutes={['/parent/report-attendance']}
>
{error ? <div className="alert alert-warning small">{error}</div> : null}
{loading ? (
<p className="text-muted small">Loading</p>
) : (
<pre className="small bg-light p-2 border rounded" style={{ maxHeight: 360, overflow: 'auto' }}>
{summary}
</pre>
)}
<p className="small text-muted mt-2 mb-0">
Submit payloads: <code>POST /api/v1/parents/attendance-reports</code>,{' '}
<code>PATCH /api/v1/parents/attendance-reports/{'{id}'}</code>.
</p>
</ParentParityShell>
)
}
{status ? <div className="alert alert-success small">{status}</div> : null}
{error ? <div className="alert alert-warning small">{error}</div> : null}
{loading ? (
<p className="text-muted small">Loading</p>
) : (
<form
className="row g-3"
onSubmit={async (e) => {
e.preventDefault()
setStatus(null)
setError(null)
const form = new FormData(e.currentTarget)
try {
const res = await submitParentAttendanceReport({
student_ids: [Number(form.get('student_id'))],
date: String(form.get('date') ?? ''),
type: String(form.get('type') ?? ''),
arrival_time: String(form.get('arrival_time') ?? ''),
dismiss_time: String(form.get('dismiss_time') ?? ''),
reason: String(form.get('reason') ?? ''),
})
setStatus(`Report submitted. Inserted: ${res.inserted ?? 0}`)
e.currentTarget.reset()
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to submit report.')
}
}}
>
<div className="col-md-6">
<label className="form-label">Student</label>
<select name="student_id" className="form-control" required>
<option value="">Select student</option>
{Array.isArray(data?.students)
? data.students.map((student: any) => (
<option key={student.id} value={student.id}>
{student.firstname} {student.lastname}
</option>
))
: null}
</select>
</div>
<div className="col-md-3">
<label className="form-label">Date</label>
<input name="date" type="date" className="form-control" defaultValue={String(data?.defaultDate ?? '')} required />
</div>
<div className="col-md-3">
<label className="form-label">Type</label>
<select name="type" className="form-control" required>
<option value="absent">Absent</option>
<option value="late">Late</option>
<option value="early_dismissal">Early dismissal</option>
</select>
</div>
<div className="col-md-6"><label className="form-label">Arrival time</label><input name="arrival_time" type="time" className="form-control" /></div>
<div className="col-md-6"><label className="form-label">Dismiss time</label><input name="dismiss_time" type="time" className="form-control" /></div>
<div className="col-12"><label className="form-label">Reason</label><textarea name="reason" className="form-control" rows={3} /></div>
<div className="col-12"><button className="btn btn-success" type="submit">Submit report</button></div>
</form>
)}
</ParentParityShell>
)
}