init project

This commit is contained in:
root
2026-04-23 00:21:02 -04:00
parent 23826c5528
commit 9191fd32f0
64 changed files with 10229 additions and 0 deletions
+437
View File
@@ -0,0 +1,437 @@
import { useEffect, useState, type FormEvent } from 'react'
import { ParentParityShell } from './ParentParityShell'
import {
fetchAttendanceReportForm,
fetchParentEnrollments,
fetchParentEventsOverview,
fetchParentInvoices,
fetchParentProfile,
fetchParentRegistrationOverview,
submitContactMessage,
} from '../../api/session'
import type { ParentEnrollmentStudent, ParentInvoiceRow } from '../../api/types'
import { useAuth } from '../../auth/AuthProvider'
/** `Views/parent/contact.php` — POST /api/v1/contact */
export function ParentContactPage() {
const { user } = useAuth()
const [name, setName] = useState('')
const [email, setEmail] = useState('')
const [subject, setSubject] = useState('')
const [message, setMessage] = useState('')
const [status, setStatus] = useState<string | null>(null)
const [error, setError] = useState<string | null>(null)
const [loading, setLoading] = useState(false)
useEffect(() => {
let cancelled = false
;(async () => {
try {
const res = await fetchParentProfile()
const p = res.profile
if (cancelled || !p) return
const fn = (p.firstname ?? '').trim()
const ln = (p.lastname ?? '').trim()
const full = `${fn} ${ln}`.trim()
if (full) setName(full)
if (p.email) setEmail(String(p.email))
} catch {
/* optional */
}
})()
return () => {
cancelled = true
}
}, [])
useEffect(() => {
if (!user?.name) return
setName((n) => n || user.name)
}, [user?.name])
async function onSubmit(e: FormEvent) {
e.preventDefault()
setLoading(true)
setError(null)
setStatus(null)
try {
const res = await submitContactMessage({ name, email, subject, message })
if (res.status) {
setStatus(res.message ?? 'Message sent.')
setSubject('')
setMessage('')
} else {
setError('Could not send message.')
}
} catch (err) {
setError(err instanceof Error ? err.message : 'Request failed.')
} finally {
setLoading(false)
}
}
return (
<ParentParityShell
title="Contact"
ciViewFile="contact.php"
legacyCiRoutes={['/parent/contact']}
>
{status ? <div className="alert alert-success small">{status}</div> : null}
{error ? <div className="alert alert-danger small">{error}</div> : null}
<form className="row g-3" onSubmit={onSubmit}>
<div className="col-md-6">
<label className="form-label" htmlFor="pc_name">
Name
</label>
<input
id="pc_name"
className="form-control"
value={name}
onChange={(e) => setName(e.target.value)}
required
minLength={3}
/>
</div>
<div className="col-md-6">
<label className="form-label" htmlFor="pc_email">
Email
</label>
<input
id="pc_email"
type="email"
className="form-control"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
/>
</div>
<div className="col-12">
<label className="form-label" htmlFor="pc_subject">
Subject
</label>
<input
id="pc_subject"
className="form-control"
value={subject}
onChange={(e) => setSubject(e.target.value)}
required
minLength={3}
/>
</div>
<div className="col-12">
<label className="form-label" htmlFor="pc_message">
Message
</label>
<textarea
id="pc_message"
className="form-control"
rows={5}
value={message}
onChange={(e) => setMessage(e.target.value)}
required
minLength={10}
/>
</div>
<div className="col-12">
<button type="submit" className="btn btn-success" disabled={loading}>
{loading ? 'Sending…' : 'Send'}
</button>
</div>
</form>
</ParentParityShell>
)
}
/** `Views/parent/enroll_classes.php` — GET/POST /api/v1/parents/enrollments */
export function ParentEnrollClassesPage() {
const [error, setError] = useState<string | null>(null)
const [loading, setLoading] = useState(true)
const [schoolYears, setSchoolYears] = useState<{ school_year: string }[]>([])
const [year, setYear] = useState<string | null>(null)
const [students, setStudents] = useState<ParentEnrollmentStudent[]>([])
useEffect(() => {
let cancelled = false
;(async () => {
setLoading(true)
try {
const data = await fetchParentEnrollments(null)
if (cancelled) return
setSchoolYears(data.schoolYears ?? [])
setYear(data.selectedYear ?? null)
setStudents(data.students ?? [])
setError(null)
} catch (e) {
if (!cancelled) setError(e instanceof Error ? e.message : 'Failed to load enrollments.')
} finally {
if (!cancelled) setLoading(false)
}
})()
return () => {
cancelled = true
}
}, [])
return (
<ParentParityShell
title="Enroll in classes"
ciViewFile="enroll_classes.php"
legacyCiRoutes={['/parent/enroll_classes']}
>
{error ? <div className="alert alert-warning small">{error}</div> : null}
{loading ? (
<p className="text-muted small">Loading</p>
) : (
<>
<p className="small text-muted mb-2">
School year: <code>{year ?? '—'}</code>. Updates use{' '}
<code>POST /api/v1/parents/enrollments</code> with enroll / withdraw payloads.
</p>
<div className="table-responsive">
<table className="table table-sm table-bordered">
<thead>
<tr>
<th>Student</th>
<th>Class</th>
</tr>
</thead>
<tbody>
{students.length === 0 ? (
<tr>
<td colSpan={2} className="text-muted">
No students for this overview.
</td>
</tr>
) : (
students.map((s) => (
<tr key={s.id}>
<td>
{s.firstname} {s.lastname}
</td>
<td>{s.class_section ?? '—'}</td>
</tr>
))
)}
</tbody>
</table>
</div>
{schoolYears.length > 1 ? (
<p className="small mb-0">Additional years in response: {schoolYears.length}</p>
) : null}
</>
)}
</ParentParityShell>
)
}
/** `Views/parent/payment_view.php` — GET /api/v1/parents/invoices */
export function ParentPaymentInvoicesPage() {
const [error, setError] = useState<string | null>(null)
const [loading, setLoading] = useState(true)
const [rows, setRows] = useState<ParentInvoiceRow[]>([])
useEffect(() => {
let cancelled = false
;(async () => {
setLoading(true)
try {
const data = await fetchParentInvoices(null)
if (cancelled) return
setRows(data.invoices ?? [])
setError(null)
} catch (e) {
if (!cancelled) setError(e instanceof Error ? e.message : 'Failed to load invoices.')
} finally {
if (!cancelled) setLoading(false)
}
})()
return () => {
cancelled = true
}
}, [])
return (
<ParentParityShell
title="Payments & invoices"
ciViewFile="payment_view.php"
legacyCiRoutes={['/parent/payment']}
>
{error ? <div className="alert alert-warning small">{error}</div> : null}
{loading ? (
<p className="text-muted small">Loading</p>
) : (
<div className="table-responsive">
<table className="table table-sm table-striped">
<thead>
<tr>
<th>#</th>
<th>Invoice</th>
<th>Balance</th>
<th>Status</th>
<th>Due</th>
</tr>
</thead>
<tbody>
{rows.length === 0 ? (
<tr>
<td colSpan={5} className="text-muted">
No invoices.
</td>
</tr>
) : (
rows.map((inv) => (
<tr key={inv.id}>
<td>{inv.id}</td>
<td>{inv.invoice_number ?? '—'}</td>
<td>
{typeof inv.balance === 'number' ? inv.balance.toFixed(2) : '—'}
</td>
<td>{inv.status ?? '—'}</td>
<td>{inv.due_date ?? '—'}</td>
</tr>
))
)}
</tbody>
</table>
</div>
)}
</ParentParityShell>
)
}
/** `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>('')
useEffect(() => {
let cancelled = false
;(async () => {
setLoading(true)
try {
const data = await fetchParentEventsOverview()
if (cancelled) return
setSummary(JSON.stringify(data, null, 2))
setError(null)
} catch (e) {
if (!cancelled) setError(e instanceof Error ? e.message : 'Failed to load events.')
} finally {
if (!cancelled) setLoading(false)
}
})()
return () => {
cancelled = true
}
}, [])
return (
<ParentParityShell
title="Event participation"
ciViewFile="event_participation.php"
legacyCiRoutes={['/parent/events']}
>
{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: 320, overflow: 'auto' }}>
{summary}
</pre>
)}
</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>('')
useEffect(() => {
let cancelled = false
;(async () => {
setLoading(true)
try {
const data = await fetchParentRegistrationOverview()
if (cancelled) return
setSummary(JSON.stringify(data, null, 2))
setError(null)
} catch (e) {
if (!cancelled) setError(e instanceof Error ? e.message : 'Failed to load registration.')
} finally {
if (!cancelled) setLoading(false)
}
})()
return () => {
cancelled = true
}
}, [])
return (
<ParentParityShell
title="Register student"
ciViewFile="register_student.php"
legacyCiRoutes={['/parent/register_student', '/parent/child_register']}
>
{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>
)}
</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>('')
useEffect(() => {
let cancelled = false
;(async () => {
setLoading(true)
try {
const data = await fetchAttendanceReportForm()
if (cancelled) return
setSummary(JSON.stringify(data, null, 2))
setError(null)
} catch (e) {
if (!cancelled)
setError(e instanceof Error ? e.message : 'Failed to load attendance report form.')
} finally {
if (!cancelled) setLoading(false)
}
})()
return () => {
cancelled = true
}
}, [])
return (
<ParentParityShell
title="Report attendance"
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>
)
}