fix: resolve all CI lint errors (9 errors → 0)
Web Client CI/CD / Lint (ESLint + TypeScript) (push) Successful in 40s
Web Client CI/CD / Build (tsc + Vite) (push) Failing after 36s
Web Client CI/CD / Deploy to shared hosting (push) Has been skipped

- Move SortTh outside AdminEmergencyContactIndexPage to fix
  react-hooks/static-components (5 errors)
- Fix ternary expressions used as statements in toggleSection
  and toggleDate (2 @typescript-eslint/no-unused-expressions errors)
- Remove stale eslint-disable-next-line react/no-danger comment
  referencing unregistered rule (1 error)
- Fix pre-existing any type in WiredParentScreens student.map
This commit is contained in:
root
2026-06-22 00:38:47 -04:00
parent 7282020444
commit 816f4cfaf0
5 changed files with 187 additions and 180 deletions
+26 -18
View File
@@ -37,6 +37,27 @@ type FlatContact = {
phone: string phone: string
} }
type SortKey = keyof Omit<FlatContact, 'contactId' | 'parentId'>
function SortTh({ col, label, sortKey, sortDir, onSort }: {
col: SortKey
label: string
sortKey: SortKey
sortDir: 'asc' | 'desc'
onSort: (col: SortKey) => void
}) {
const active = sortKey === col
return (
<th
role="button"
style={{ cursor: 'pointer', userSelect: 'none', whiteSpace: 'nowrap' }}
onClick={() => onSort(col)}
>
{label} <span className="text-muted small">{active ? (sortDir === 'asc' ? '▲' : '▼') : '⇅'}</span>
</th>
)
}
export function AdminEmergencyContactIndexPage() { export function AdminEmergencyContactIndexPage() {
const [groups, setGroups] = useState<EmergencyContactGroupRow[]>([]) const [groups, setGroups] = useState<EmergencyContactGroupRow[]>([])
const [error, setError] = useState<string | null>(null) const [error, setError] = useState<string | null>(null)
@@ -97,19 +118,6 @@ export function AdminEmergencyContactIndexPage() {
}) })
}, [flatRows, search, sortKey, sortDir]) }, [flatRows, search, sortKey, sortDir])
function SortTh({ col, label }: { col: typeof sortKey; label: string }) {
const active = sortKey === col
return (
<th
role="button"
style={{ cursor: 'pointer', userSelect: 'none', whiteSpace: 'nowrap' }}
onClick={() => toggleSort(col)}
>
{label} <span className="text-muted small">{active ? (sortDir === 'asc' ? '▲' : '▼') : '⇅'}</span>
</th>
)
}
return ( return (
<PageShell title="Emergency Contact Information"> <PageShell title="Emergency Contact Information">
{error ? <div className="alert alert-danger">{error}</div> : null} {error ? <div className="alert alert-danger">{error}</div> : null}
@@ -132,11 +140,11 @@ export function AdminEmergencyContactIndexPage() {
<table className="table table-bordered table-striped align-middle"> <table className="table table-bordered table-striped align-middle">
<thead className="table-dark"> <thead className="table-dark">
<tr> <tr>
<SortTh col="parentName" label="Parent Name" /> <SortTh col="parentName" label="Parent Name" sortKey={sortKey} sortDir={sortDir} onSort={toggleSort} />
<SortTh col="students" label="Students" /> <SortTh col="students" label="Students" sortKey={sortKey} sortDir={sortDir} onSort={toggleSort} />
<SortTh col="contactName" label="Contact Name" /> <SortTh col="contactName" label="Contact Name" sortKey={sortKey} sortDir={sortDir} onSort={toggleSort} />
<SortTh col="relation" label="Relation" /> <SortTh col="relation" label="Relation" sortKey={sortKey} sortDir={sortDir} onSort={toggleSort} />
<SortTh col="phone" label="Phone" /> <SortTh col="phone" label="Phone" sortKey={sortKey} sortDir={sortDir} onSort={toggleSort} />
<th>Actions</th> <th>Actions</th>
</tr> </tr>
</thead> </thead>
+1 -1
View File
@@ -1350,7 +1350,7 @@ export function AdminClassAssignmentPage() {
function toggleSection(key: string) { function toggleSection(key: string) {
setExpandedSections((prev) => { setExpandedSections((prev) => {
const next = new Set(prev) const next = new Set(prev)
next.has(key) ? next.delete(key) : next.add(key) if (next.has(key)) next.delete(key); else next.add(key)
return next return next
}) })
} }
@@ -116,7 +116,6 @@ export function FamilyLegacyImportPage() {
{meta?.instructions_html ? ( {meta?.instructions_html ? (
<div <div
className="small" className="small"
// eslint-disable-next-line react/no-danger -- API-provided HTML when present
dangerouslySetInnerHTML={{ __html: meta.instructions_html }} dangerouslySetInnerHTML={{ __html: meta.instructions_html }}
/> />
) : ( ) : (
+159 -159
View File
@@ -4,19 +4,19 @@ import {
fetchAttendanceReportForm, fetchAttendanceReportForm,
fetchParentEnrollments, fetchParentEnrollments,
fetchParentEventsOverview, fetchParentEventsOverview,
fetchParentInvoices, fetchParentInvoices,
fetchParentProfile, fetchParentProfile,
fetchParentRegistrationOverview, fetchParentRegistrationOverview,
submitParentAttendanceReport, submitParentAttendanceReport,
submitContactMessage, submitContactMessage,
} from '../../api/session' } from '../../api/session'
import type { import type {
ParentAttendanceReportFormResponse, ParentAttendanceReportFormResponse,
ParentEnrollmentStudent, ParentEnrollmentStudent,
ParentEventsOverviewResponse, ParentEventsOverviewResponse,
ParentInvoiceRow, ParentInvoiceRow,
ParentRegistrationOverviewResponse, ParentRegistrationOverviewResponse,
} from '../../api/types' } from '../../api/types'
import { useAuth } from '../../auth/AuthProvider' import { useAuth } from '../../auth/AuthProvider'
/** `Views/parent/contact.php` — POST /api/v1/contact */ /** `Views/parent/contact.php` — POST /api/v1/contact */
@@ -307,19 +307,19 @@ export function ParentPaymentInvoicesPage() {
} }
/** `Views/parent/event_participation.php` — GET/POST /api/v1/parents/events … */ /** `Views/parent/event_participation.php` — GET/POST /api/v1/parents/events … */
export function ParentEventsPage() { export function ParentEventsPage() {
const [error, setError] = useState<string | null>(null) const [error, setError] = useState<string | null>(null)
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const [data, setData] = useState<ParentEventsOverviewResponse | null>(null) const [data, setData] = useState<ParentEventsOverviewResponse | null>(null)
useEffect(() => { useEffect(() => {
let cancelled = false let cancelled = false
;(async () => { ;(async () => {
setLoading(true) setLoading(true)
try { try {
const data = await fetchParentEventsOverview() const data = await fetchParentEventsOverview()
if (cancelled) return if (cancelled) return
setData(data) setData(data)
setError(null) setError(null)
} catch (e) { } catch (e) {
if (!cancelled) setError(e instanceof Error ? e.message : 'Failed to load events.') if (!cancelled) setError(e instanceof Error ? e.message : 'Failed to load events.')
@@ -342,43 +342,43 @@ export function ParentEventsPage() {
{loading ? ( {loading ? (
<p className="text-muted small">Loading</p> <p className="text-muted small">Loading</p>
) : ( ) : (
<div className="table-responsive"> <div className="table-responsive">
<table className="table table-sm table-striped align-middle"> <table className="table table-sm table-striped align-middle">
<thead> <thead>
<tr><th>Event</th><th>Date</th><th>Description</th></tr> <tr><th>Event</th><th>Date</th><th>Description</th></tr>
</thead> </thead>
<tbody> <tbody>
{(data?.activeEvents ?? []).length === 0 ? ( {(data?.activeEvents ?? []).length === 0 ? (
<tr><td colSpan={3} className="text-muted">No active events.</td></tr> <tr><td colSpan={3} className="text-muted">No active events.</td></tr>
) : (data?.activeEvents ?? []).map((event) => ( ) : (data?.activeEvents ?? []).map((event) => (
<tr key={String(event.id ?? event.title ?? event.event_name)}> <tr key={String(event.id ?? event.title ?? event.event_name)}>
<td>{event.title ?? event.event_name ?? '—'}</td> <td>{event.title ?? event.event_name ?? '—'}</td>
<td>{event.event_date ?? event.start_date ?? '—'}</td> <td>{event.event_date ?? event.start_date ?? '—'}</td>
<td>{event.description ?? '—'}</td> <td>{event.description ?? '—'}</td>
</tr> </tr>
))} ))}
</tbody> </tbody>
</table> </table>
</div> </div>
)} )}
</ParentParityShell> </ParentParityShell>
) )
} }
/** `Views/parent/register_student.php` — GET /api/v1/parents/registration */ /** `Views/parent/register_student.php` — GET /api/v1/parents/registration */
export function ParentRegisterStudentPage() { export function ParentRegisterStudentPage() {
const [error, setError] = useState<string | null>(null) const [error, setError] = useState<string | null>(null)
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const [data, setData] = useState<ParentRegistrationOverviewResponse | null>(null) const [data, setData] = useState<ParentRegistrationOverviewResponse | null>(null)
useEffect(() => { useEffect(() => {
let cancelled = false let cancelled = false
;(async () => { ;(async () => {
setLoading(true) setLoading(true)
try { try {
const data = await fetchParentRegistrationOverview() const data = await fetchParentRegistrationOverview()
if (cancelled) return if (cancelled) return
setData(data) setData(data)
setError(null) setError(null)
} catch (e) { } catch (e) {
if (!cancelled) setError(e instanceof Error ? e.message : 'Failed to load registration.') if (!cancelled) setError(e instanceof Error ? e.message : 'Failed to load registration.')
@@ -401,68 +401,68 @@ export function ParentRegisterStudentPage() {
{loading ? ( {loading ? (
<p className="text-muted small">Loading</p> <p className="text-muted small">Loading</p>
) : ( ) : (
<> <>
<div className="d-flex flex-wrap gap-2 mb-3"> <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-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> <a className="btn btn-outline-success btn-sm" href="/app/parent/add-emergency-form">Add emergency contact</a>
</div> </div>
<h3 className="h6">Students</h3> <h3 className="h6">Students</h3>
<div className="table-responsive mb-3"> <div className="table-responsive mb-3">
<table className="table table-sm table-striped"> <table className="table table-sm table-striped">
<thead><tr><th>Name</th><th>Grade</th><th>Class</th><th>Status</th></tr></thead> <thead><tr><th>Name</th><th>Grade</th><th>Class</th><th>Status</th></tr></thead>
<tbody> <tbody>
{(data?.existingKids ?? []).length === 0 ? ( {(data?.existingKids ?? []).length === 0 ? (
<tr><td colSpan={4} className="text-muted">No students registered.</td></tr> <tr><td colSpan={4} className="text-muted">No students registered.</td></tr>
) : (data?.existingKids ?? []).map((student) => ( ) : (data?.existingKids ?? []).map((student) => (
<tr key={student.id}> <tr key={student.id}>
<td>{student.firstname} {student.lastname}</td> <td>{student.firstname} {student.lastname}</td>
<td>{student.registration_grade ?? '—'}</td> <td>{student.registration_grade ?? '—'}</td>
<td>{student.class_section ?? '—'}</td> <td>{student.class_section ?? '—'}</td>
<td>{student.admission_status ?? student.enrollment_status ?? '—'}</td> <td>{student.admission_status ?? student.enrollment_status ?? '—'}</td>
</tr> </tr>
))} ))}
</tbody> </tbody>
</table> </table>
</div> </div>
<h3 className="h6">Emergency contacts</h3> <h3 className="h6">Emergency contacts</h3>
<div className="table-responsive"> <div className="table-responsive">
<table className="table table-sm table-striped"> <table className="table table-sm table-striped">
<thead><tr><th>Name</th><th>Phone</th><th>Email</th><th>Relation</th></tr></thead> <thead><tr><th>Name</th><th>Phone</th><th>Email</th><th>Relation</th></tr></thead>
<tbody> <tbody>
{(data?.emergencies ?? []).length === 0 ? ( {(data?.emergencies ?? []).length === 0 ? (
<tr><td colSpan={4} className="text-muted">No emergency contacts.</td></tr> <tr><td colSpan={4} className="text-muted">No emergency contacts.</td></tr>
) : (data?.emergencies ?? []).map((contact) => ( ) : (data?.emergencies ?? []).map((contact) => (
<tr key={contact.id}> <tr key={contact.id}>
<td>{contact.name}</td> <td>{contact.name}</td>
<td>{contact.cellphone}</td> <td>{contact.cellphone}</td>
<td>{contact.email ?? '—'}</td> <td>{contact.email ?? '—'}</td>
<td>{contact.relation ?? '—'}</td> <td>{contact.relation ?? '—'}</td>
</tr> </tr>
))} ))}
</tbody> </tbody>
</table> </table>
</div> </div>
</> </>
)} )}
</ParentParityShell> </ParentParityShell>
) )
} }
/** `Views/parent/report_attendance.php` — form: GET /api/v1/parents/attendance-reports/form */ /** `Views/parent/report_attendance.php` — form: GET /api/v1/parents/attendance-reports/form */
export function ParentReportAttendancePage() { export function ParentReportAttendancePage() {
const [error, setError] = useState<string | null>(null) const [error, setError] = useState<string | null>(null)
const [status, setStatus] = useState<string | null>(null) const [status, setStatus] = useState<string | null>(null)
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const [data, setData] = useState<ParentAttendanceReportFormResponse | null>(null) const [data, setData] = useState<ParentAttendanceReportFormResponse | null>(null)
useEffect(() => { useEffect(() => {
let cancelled = false let cancelled = false
;(async () => { ;(async () => {
setLoading(true) setLoading(true)
try { try {
const data = await fetchAttendanceReportForm() const data = await fetchAttendanceReportForm()
if (cancelled) return if (cancelled) return
setData(data) setData(data)
setError(null) setError(null)
} catch (e) { } catch (e) {
if (!cancelled) if (!cancelled)
@@ -482,65 +482,65 @@ export function ParentReportAttendancePage() {
ciViewFile="report_attendance.php" ciViewFile="report_attendance.php"
legacyCiRoutes={['/parent/report-attendance']} legacyCiRoutes={['/parent/report-attendance']}
> >
{status ? <div className="alert alert-success small">{status}</div> : null} {status ? <div className="alert alert-success small">{status}</div> : null}
{error ? <div className="alert alert-warning small">{error}</div> : null} {error ? <div className="alert alert-warning small">{error}</div> : null}
{loading ? ( {loading ? (
<p className="text-muted small">Loading</p> <p className="text-muted small">Loading</p>
) : ( ) : (
<form <form
className="row g-3" className="row g-3"
onSubmit={async (e) => { onSubmit={async (e) => {
e.preventDefault() e.preventDefault()
setStatus(null) setStatus(null)
setError(null) setError(null)
const form = new FormData(e.currentTarget) const form = new FormData(e.currentTarget)
try { try {
const res = await submitParentAttendanceReport({ const res = await submitParentAttendanceReport({
student_ids: [Number(form.get('student_id'))], student_ids: [Number(form.get('student_id'))],
date: String(form.get('date') ?? ''), date: String(form.get('date') ?? ''),
type: String(form.get('type') ?? ''), type: String(form.get('type') ?? ''),
arrival_time: String(form.get('arrival_time') ?? ''), arrival_time: String(form.get('arrival_time') ?? ''),
dismiss_time: String(form.get('dismiss_time') ?? ''), dismiss_time: String(form.get('dismiss_time') ?? ''),
reason: String(form.get('reason') ?? ''), reason: String(form.get('reason') ?? ''),
}) })
setStatus(`Report submitted. Inserted: ${res.inserted ?? 0}`) setStatus(`Report submitted. Inserted: ${res.inserted ?? 0}`)
e.currentTarget.reset() e.currentTarget.reset()
} catch (err) { } catch (err) {
setError(err instanceof Error ? err.message : 'Failed to submit report.') setError(err instanceof Error ? err.message : 'Failed to submit report.')
} }
}} }}
> >
<div className="col-md-6"> <div className="col-md-6">
<label className="form-label">Student</label> <label className="form-label">Student</label>
<select name="student_id" className="form-control" required> <select name="student_id" className="form-control" required>
<option value="">Select student</option> <option value="">Select student</option>
{Array.isArray(data?.students) {Array.isArray(data?.students)
? data.students.map((student: any) => ( ? data.students.map((student: { id: number; firstname: string; lastname: string }) => (
<option key={student.id} value={student.id}> <option key={student.id} value={student.id}>
{student.firstname} {student.lastname} {student.firstname} {student.lastname}
</option> </option>
)) ))
: null} : null}
</select> </select>
</div> </div>
<div className="col-md-3"> <div className="col-md-3">
<label className="form-label">Date</label> <label className="form-label">Date</label>
<input name="date" type="date" className="form-control" defaultValue={String(data?.defaultDate ?? '')} required /> <input name="date" type="date" className="form-control" defaultValue={String(data?.defaultDate ?? '')} required />
</div> </div>
<div className="col-md-3"> <div className="col-md-3">
<label className="form-label">Type</label> <label className="form-label">Type</label>
<select name="type" className="form-control" required> <select name="type" className="form-control" required>
<option value="absent">Absent</option> <option value="absent">Absent</option>
<option value="late">Late</option> <option value="late">Late</option>
<option value="early_dismissal">Early dismissal</option> <option value="early_dismissal">Early dismissal</option>
</select> </select>
</div> </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">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-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"><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> <div className="col-12"><button className="btn btn-success" type="submit">Submit report</button></div>
</form> </form>
)} )}
</ParentParityShell> </ParentParityShell>
) )
} }
+1 -1
View File
@@ -113,7 +113,7 @@ export function SlipPreviewListPage() {
function toggleDate(dateKey: string) { function toggleDate(dateKey: string) {
setExpandedDates((prev) => { setExpandedDates((prev) => {
const next = new Set(prev) const next = new Set(prev)
next.has(dateKey) ? next.delete(dateKey) : next.add(dateKey) if (next.has(dateKey)) next.delete(dateKey); else next.add(dateKey)
return next return next
}) })
} }