import { Link } from 'react-router-dom' import { useEffect, useMemo, useRef, useState, type FormEvent, type ReactNode } from 'react' import { useSchoolYearOptions } from '../hooks/useSchoolYearOptions' import type { LegacySchoolYearValue } from '../lib/schoolYearOptions' export type CiFlashMessage = { id?: string type: 'success' | 'error' | 'info' | 'warning' message: ReactNode } const alertType = { success: 'success', error: 'danger', info: 'info', warning: 'warning', } as const export function CiFlashMessages({ messages, autoHideMs = 4000, }: { messages: CiFlashMessage[] autoHideMs?: number }) { const [visible, setVisible] = useState(messages) useEffect(() => { setVisible(messages) }, [messages]) useEffect(() => { if (autoHideMs <= 0 || visible.length === 0) return const timeout = window.setTimeout(() => setVisible([]), autoHideMs) return () => window.clearTimeout(timeout) }, [autoHideMs, visible.length]) return ( <> {visible.map((item, index) => (
{item.message}
))} ) } export function CiAcademicFilter({ schoolYears, selectedYear, selectedSemester, classSectionId, onApply, }: { schoolYears?: LegacySchoolYearValue[] selectedYear?: string | null selectedSemester?: string | null classSectionId?: string | number | null onApply?: (values: { schoolYear: string; semester: string; classSectionId?: string }) => void }) { const { options, selectedYear: defaultYear } = useSchoolYearOptions({ legacyYears: schoolYears, preferredYear: selectedYear, }) const [schoolYear, setSchoolYear] = useState(selectedYear ?? defaultYear) const [semester, setSemester] = useState(selectedSemester ?? '') useEffect(() => { setSchoolYear(selectedYear ?? defaultYear) }, [defaultYear, selectedYear]) useEffect(() => { setSemester(selectedSemester ?? '') }, [selectedSemester]) return (
{ event.preventDefault() onApply?.({ schoolYear, semester, classSectionId: classSectionId == null ? undefined : String(classSectionId), }) }} >
{' '}
) } export function CiStandardStudentTable({ rows, customHeaders = [], customKeys = [], emptyText = 'No rows found.', }: { rows: Record[] customHeaders?: string[] customKeys?: string[] emptyText?: string }) { return (
{customHeaders.map((header) => ( ))} {rows.length === 0 ? ( ) : ( rows.map((row, index) => ( {customKeys.map((key) => ( ))} )) )}
# School ID First Name Last Name Gender Age Grade{header}
{emptyText}
{index + 1} {display(row.school_id)} {display(row.first_name ?? row.firstname)} {display(row.last_name ?? row.lastname)} {display(row.gender)} {display(row.age)} {display(row.Grade ?? row.grade ?? row.registration_grade)}{display(row[key])}
) } const publicGradeOptions = [ ['NA', 'Not enrolled in public school'], ['KG', 'Pre-K / Kindergarten'], ['1', 'Grade 1'], ['2', 'Grade 2'], ['3', 'Grade 3'], ['4', 'Grade 4'], ['5', 'Grade 5'], ['6', 'Grade 6'], ['7', 'Grade 7'], ['8', 'Grade 8'], ['9', 'Grade 9'], ['10', 'Grade 10'], ['11', 'Grade 11'], ['12', 'Grade 12'], ] const alRahmaGradeOptions = [ ['KG', 'Pre-K / Kindergarten'], ['1', 'Grade 1'], ['2', 'Grade 2'], ['3', 'Grade 3'], ['4', 'Grade 4'], ['5', 'Grade 5'], ['6', 'Grade 6'], ['7', 'Grade 7'], ['8', 'Grade 8'], ['9', 'Grade 9'], ['10', 'Youth'], ] export const medicalConditionOptions = [ 'None', 'ADHD (Attention-Deficit/Hyperactivity Disorder)', 'Anxiety or Emotional Disorders', 'Asthma', 'Autism Spectrum Disorder (ASD)', 'Behavioral or Conduct Disorders', 'Blindness / Vision Impairment', 'Celiac Disease (Gluten Intolerance)', 'Cerebral Palsy', 'Cystic Fibrosis', 'Depression', 'Diabetes (Type 1 or Type 2)', 'Down Syndrome', 'Dyslexia or Learning Disabilities', 'Eating Disorders', 'Eczema / Severe Skin Conditions', 'Epilepsy / Seizure Disorders', 'Hearing Impairments / Deafness', 'Heart Conditions (congenital or acquired)', 'Hemophilia / Bleeding Disorders', 'Kidney Disease', 'Migraines / Chronic Headaches', 'Obsessive-Compulsive Disorder (OCD)', 'Physical Disabilities / Mobility Impairments', 'PTSD (Post-Traumatic Stress Disorder)', 'Sickle Cell Anemia', 'Speech and Language Disorders', 'Thyroid Disorders', 'Tourette Syndrome', 'Traumatic Brain Injury (TBI)', 'Rheumatic diseases', "Ulcerative Colitis / Crohn's Disease", 'Other', ] export const allergyOptions = [ 'None', 'Animal Dander (cats, dogs, etc.)', 'Antibiotics', 'Bee stings', 'Cockroach', 'Corn', 'Dust Mites', 'Egg', 'Fire ant stings', 'Fish', 'Fragrances / Perfumes', 'Latex', 'Milk / Dairy', 'Mold', 'Mosquito bites', 'Peanut', 'Pollen (grass, tree, weed)', 'Sesame', 'Shellfish (shrimp, crab, lobster, etc.)', 'Soy', 'Tree Nuts (almond, cashew, walnut, etc.)', 'Wasp stings', 'Wheat / Gluten', 'Other', ] export function CiStudentFormPartial({ defaultValues, showRemoveButton = false, }: { defaultValues?: { firstname?: string | null lastname?: string | null dob?: string | null gender?: string | null registration_grade?: string | null photo_consent?: boolean | string | null last_year?: 'yes' | 'no' | string | null allergies?: string[] medical_conditions?: string[] } showRemoveButton?: boolean }) { const [lastYear, setLastYear] = useState(defaultValues?.last_year ?? (defaultValues?.registration_grade ? 'yes' : '')) const [selectedGrade, setSelectedGrade] = useState(defaultValues?.registration_grade ?? '') const gradeOptions = lastYear === 'yes' ? alRahmaGradeOptions : lastYear === 'no' ? publicGradeOptions : [] const gradeLabel = lastYear === 'yes' ? 'Al Rahma School Last Year Grade' : lastYear === 'no' ? 'Public School Last Year Grade' : 'Last Year Grade' const defaultMedical = useMemo(() => new Set(defaultValues?.medical_conditions ?? []), [defaultValues?.medical_conditions]) const defaultAllergies = useMemo(() => new Set(defaultValues?.allergies ?? []), [defaultValues?.allergies]) return (
{showRemoveButton ? (
) } export function CiEmergencyContactFormPartial({ defaultValues, }: { defaultValues?: { name?: string | null cellphone?: string | null email?: string | null relation?: string | null } }) { return (
Emergency Contact Information
) } export function CiNoInvoiceFound({ message = 'No invoice found.' }: { message?: string }) { return (
Error

{message}

) } export function CiPublicFooter({ year = new Date().getFullYear() }: { year?: number }) { const footerRef = useRef(null) useEffect(() => { const root = footerRef.current if (!root) return const links = root.querySelectorAll('a.pdf-link') const controllers: AbortController[] = [] links.forEach((link) => { const pdfUrl = link.getAttribute('href') if (!pdfUrl) return const ac = new AbortController() controllers.push(ac) fetch(pdfUrl, { method: 'HEAD', signal: ac.signal }) .then((response) => { if (!response.ok) { link.style.opacity = '0.7' link.title = 'File might be temporarily unavailable' console.warn('PDF might be missing:', pdfUrl) link.addEventListener('click', function onClick(e) { if (!confirm('The PDF file might be temporarily unavailable. Try to open it anyway?')) { e.preventDefault() } }) } }) .catch(() => { link.style.opacity = '0.7' link.title = 'File check failed' }) }) return () => controllers.forEach((c) => c.abort()) }, []) return ( ) } export function CiBackFooter({ year = new Date().getFullYear() }: { year?: number }) { return (

© {year} Al Rahma Sunday School by{' '} ISGL. {' '}All Rights Reserved.

) } export function CiAvatarMenu({ name, email, currentRole, roles = [], onSwitchRole, onLogout, }: { name?: string | null email?: string | null currentRole?: string | null roles?: string[] onSwitchRole?: (role: string) => void onLogout?: () => void }) { const [open, setOpen] = useState(false) const [roleOpen, setRoleOpen] = useState(false) const [styleOpen, setStyleOpen] = useState(false) const initials = userInitials(name, email) const normalizedRole = currentRole || 'guest' const otherRoles = roles.filter((role) => role && role !== normalizedRole) return (
{initials}
{name || 'User'}
{normalizedRole} {email ? ` · ${email}` : ''}
{otherRoles.length > 0 ? ( <> {roleOpen ? (
{otherRoles.map((role) => ( ))}
) : null}
) : null} {styleOpen ? (
Menu
{['emerald', 'white', 'dark'].map((menu) => ( {humanRole(menu)} ))}
) : null}
) } export function CiParentSidebar() { const links = [ ['/app/parent/home', 'bi bi-house-door', 'Dashboard'], ['/app/parent/register-student', 'bi bi-person-plus', 'Register Kids'], ['/app/parent/enroll-classes', 'bi bi-pencil-square', 'Enroll in Classes'], ['/app/parent/attendance', 'bi bi-calendar-check', 'Attendance'], ['/app/parent/scores', 'bi bi-clipboard-data', 'Scores'], ['/app/parent/payment', 'bi bi-credit-card', 'Payment'], ['/app/parent/message', 'bi bi-chat-dots', 'Messages'], ['/app/parent/calendar', 'bi bi-calendar', 'Calendar'], ['/app/parent/contact', 'bi bi-telephone', 'Contact Us'], ] as const return ( ) } export function CiAssignClassTeacherModal({ teacherName = '', teacherRole = '', classes, onSubmit, }: { teacherName?: string teacherRole?: string classes: Array<{ id: number | string; name: string }> onSubmit?: (classSectionId: string) => void }) { const [classSectionId, setClassSectionId] = useState('') return (
{ event.preventDefault() if (classSectionId) onSubmit?.(classSectionId) }} >
Assign Class
) } export function CiAssignClassStudentModal({ studentName = '', classes, onSubmit, }: { studentName?: string classes: Array<{ id: number | string; name: string }> onSubmit?: (classSectionId: string) => void }) { const [classSectionId, setClassSectionId] = useState('') return (
{ event.preventDefault() if (classSectionId) onSubmit?.(classSectionId) }} >
Assign Class to Student
) } type AttendanceNoticePayload = { student_name?: string class_section_name?: string date?: string date_fmt?: string reason?: string teacher_name?: string school_name?: string } export function buildAttendanceNoticeSubject(subjectType: string, payload: AttendanceNoticePayload) { const base = payload.student_name ? ` - ${payload.student_name}` : '' const dateLabel = payload.date_fmt || formatNoticeDate(payload.date) const date = dateLabel ? ` (${dateLabel})` : '' if (subjectType === 'Absent') return `Attendance Notice: Absent${base}${date}` if (subjectType === 'Late') return `Attendance Notice: Late${base}${date}` return `Attendance Notice${base}${date}` } export function buildAttendanceNoticeMessage(subjectType: string, payload: AttendanceNoticePayload) { const name = payload.student_name || 'your child' const date = payload.date_fmt || formatNoticeDate(payload.date) || '' const cls = payload.class_section_name ? ` (${payload.class_section_name})` : '' const reason = payload.reason ? ` Reason: ${payload.reason}.` : '' const teacher = payload.teacher_name ? `\nTeacher: ${payload.teacher_name}` : '' const school = payload.school_name || 'Our School' if (subjectType === 'Absent') { return `Dear Parent/Guardian,\n\nThis is to inform you that ${name}${cls} was marked Absent on ${date}.${reason}\n\nIf this was an error or there is additional context you would like to share, please reply to this email.\n\nThank you,\n${school}${teacher}\n` } if (subjectType === 'Late') { return `Dear Parent/Guardian,\n\nThis is to notify you that ${name}${cls} was marked Late on ${date}.${reason}\n\nPlease help ensure timely arrival. If you have questions, reply to this email.\n\nThank you,\n${school}${teacher}\n` } return `Dear Parent/Guardian,\n\nWe are reaching out regarding ${name}${cls} on ${date}. Please see attendance details in the subject line.\n\nIf you have any questions, reply to this email.\n\nThank you,\n${school}${teacher}\n` } export function CiAttendanceNotificationModal({ payload = {}, onSubmit, }: { payload?: AttendanceNoticePayload & { student_id?: number | string; parent_email?: string } onSubmit?: (values: { to: string; subjectType: string; subject: string; message: string }) => Promise | void }) { const [subjectType, setSubjectType] = useState('Absent') const [to, setTo] = useState(payload.parent_email ?? '') const [subject, setSubject] = useState(() => buildAttendanceNoticeSubject('Absent', payload)) const [message, setMessage] = useState(() => buildAttendanceNoticeMessage('Absent', payload)) function updateType(nextType: string) { setSubjectType(nextType) setSubject(buildAttendanceNoticeSubject(nextType, payload)) setMessage(buildAttendanceNoticeMessage(nextType, payload)) } async function submit(event: FormEvent) { event.preventDefault() await onSubmit?.({ to, subjectType, subject, message }) } return (
Notify Parent
setTo(event.target.value)} required />
setSubject(event.target.value)} required />