diff --git a/app/Controllers/View/AdministratorController.php b/app/Controllers/View/AdministratorController.php index 812b3bd..0a33ba8 100644 --- a/app/Controllers/View/AdministratorController.php +++ b/app/Controllers/View/AdministratorController.php @@ -540,7 +540,7 @@ class AdministratorController extends BaseController public function teacherSubmissionsReport() { - $semester = (string) (getSemester() ?? $this->semester ?? ''); + $semester = (string) ($this->semester !== '' ? $this->semester : (getSemester() ?? $this->semester ?? '')); $schoolYear = trim((string) ($this->request->getGet('school_year') ?? '')); if ($schoolYear === '') { $schoolYear = $this->currentSchoolYearName((string) ($this->schoolYear ?? '')); @@ -561,7 +561,7 @@ class AdministratorController extends BaseController { $result = service('teacherSubmissionReport')->sendNotifications( (array) $this->request->getPost(), - (string) (getSemester() ?? $this->semester ?? ''), + (string) ($this->semester !== '' ? $this->semester : (getSemester() ?? '')), (int) (session()->get('user_id') ?? 0) ); diff --git a/app/Controllers/View/ParentController.php b/app/Controllers/View/ParentController.php index 95a4572..a120aef 100644 --- a/app/Controllers/View/ParentController.php +++ b/app/Controllers/View/ParentController.php @@ -304,6 +304,12 @@ class ParentController extends BaseController foreach ($students as &$student) { $studentId = $student['id']; $student['age'] = $this->calculateAgeAsOfSchoolYearStartYear($student['dob'] ?? null, $selectedYear); + $student['allergies'] = $this->allergyModel + ->where('student_id', (int) $studentId) + ->findColumn('allergy') ?? []; + $student['medical_conditions'] = $this->medicalConditionModel + ->where('student_id', (int) $studentId) + ->findColumn('condition_name') ?? []; // Get class section info (can be multiple sections like Grade + Arabic) $classSections = $this->studentClassModel->getClassSectionsByStudentId($studentId, $selectedYear, true); @@ -816,66 +822,38 @@ class ParentController extends BaseController continue; } - $firstName = $this->normalizeEnrollmentStudentName((string) ($fields['firstname'] ?? '')); - $lastName = $this->normalizeEnrollmentStudentName((string) ($fields['lastname'] ?? '')); - $dob = trim((string) ($fields['dob'] ?? '')); - $gender = trim((string) ($fields['gender'] ?? '')); - $registrationGrade = trim((string) ($fields['registration_grade'] ?? '')); + $studentLabel = trim((string) ($existing['firstname'] ?? '') . ' ' . (string) ($existing['lastname'] ?? '')) + ?: 'Student ID ' . $studentId; $photoConsent = (string) ($fields['photo_consent'] ?? ''); - $studentLabel = trim($firstName . ' ' . $lastName) ?: 'Student ID ' . $studentId; - - try { - $this->validateNames($firstName); - $this->validateNames($lastName); - } catch (InvalidArgumentException $e) { - $errors[] = $studentLabel . ': ' . $e->getMessage(); - continue; - } - - if (! in_array($gender, ['Male', 'Female'], true)) { - $errors[] = $studentLabel . ': gender is required.'; - continue; - } if ($photoConsent !== '0' && $photoConsent !== '1') { $errors[] = $studentLabel . ': photo consent is required.'; continue; } - if ($registrationGrade === '' || mb_strlen($registrationGrade) > 50) { - $errors[] = $studentLabel . ': registration grade is required.'; - continue; - } - - $dobObj = \DateTimeImmutable::createFromFormat('!Y-m-d', $dob, new \DateTimeZone('UTC')); - $dobErrors = \DateTimeImmutable::getLastErrors(); - $dobWarningCount = is_array($dobErrors) ? (int) ($dobErrors['warning_count'] ?? 0) : 0; - $dobErrorCount = is_array($dobErrors) ? (int) ($dobErrors['error_count'] ?? 0) : 0; - if ($dobObj === false || $dobWarningCount > 0 || $dobErrorCount > 0) { - $errors[] = $studentLabel . ': date of birth must use YYYY-MM-DD format.'; - continue; - } - - $validation = $this->validateDobAge( - $dob, - $this->registrationMinimumAgeDeadline($schoolYear), - 5, - 18, - $this->schoolYearAgeDeadline($schoolYear) + $medicalConditions = $this->normalizeEnrollmentHealthSelections( + $fields['medical_conditions'] ?? [], + (string) ($fields['medical_condition_other'] ?? '') ); - if (! $validation['isValid']) { - $errors[] = $studentLabel . ': ' . $validation['message'] . '.'; + $allergies = $this->normalizeEnrollmentHealthSelections( + $fields['allergies'] ?? [], + (string) ($fields['allergy_other'] ?? '') + ); + + if ($medicalConditions === []) { + $errors[] = $studentLabel . ': medical conditions are required.'; + continue; + } + + if ($allergies === []) { + $errors[] = $studentLabel . ': allergies are required.'; continue; } $updates[$studentId] = [ - 'firstname' => $firstName, - 'lastname' => $lastName, - 'dob' => $dobObj->format('Y-m-d'), - 'age' => $this->calculateAgeAsOfSchoolYearStartYear($dobObj->format('Y-m-d'), $schoolYear), - 'gender' => $gender, - 'registration_grade' => $registrationGrade, 'photo_consent' => (int) $photoConsent, + 'medical_conditions' => $medicalConditions, + 'allergies' => $allergies, ]; } @@ -884,14 +862,62 @@ class ParentController extends BaseController } foreach ($updates as $studentId => $payload) { - if (! $this->studentModel->update($studentId, $payload)) { + if (! $this->studentModel->update($studentId, ['photo_consent' => $payload['photo_consent']])) { $errors[] = 'Student ID ' . $studentId . ': student information could not be updated.'; + continue; + } + + $this->medicalConditionModel->where('student_id', $studentId)->delete(); + foreach ($payload['medical_conditions'] as $condition) { + $this->medicalConditionModel->insert([ + 'student_id' => $studentId, + 'condition_name' => $condition, + ]); + } + + $this->allergyModel->where('student_id', $studentId)->delete(); + foreach ($payload['allergies'] as $allergy) { + $this->allergyModel->insert([ + 'student_id' => $studentId, + 'allergy' => $allergy, + ]); } } return $errors; } + /** + * @param mixed $selected + * @return list + */ + private function normalizeEnrollmentHealthSelections($selected, string $otherText): array + { + $values = []; + foreach ((array) $selected as $value) { + $value = trim((string) $value); + if ($value === '') { + continue; + } + $values[] = $value; + } + + $otherText = trim($otherText); + if (in_array('Other', $values, true)) { + $values = array_values(array_filter($values, static fn(string $value): bool => $value !== 'Other')); + if ($otherText !== '') { + $values[] = mb_substr($otherText, 0, 100); + } + } + + $unique = []; + foreach ($values as $value) { + $unique[$value] = $value; + } + + return array_values($unique); + } + private function normalizeEnrollmentStudentName(string $name): string { $name = trim(preg_replace('/\s+/', ' ', $name) ?? ''); diff --git a/app/Services/SchoolYearClosingService.php b/app/Services/SchoolYearClosingService.php index f951496..9600c39 100644 --- a/app/Services/SchoolYearClosingService.php +++ b/app/Services/SchoolYearClosingService.php @@ -587,6 +587,9 @@ final class SchoolYearClosingService $hasEventOnly = $this->db->fieldExists('is_event_only', 'student_class'); $hasActive = $this->db->fieldExists('is_active', 'students'); + $hasEnrollments = $this->db->tableExists('enrollments'); + $hasEnrollmentStatus = $hasEnrollments && $this->db->fieldExists('enrollment_status', 'enrollments'); + $hasEnrollmentWithdrawn = $hasEnrollments && $this->db->fieldExists('is_withdrawn', 'enrollments'); $hasDob = $this->db->fieldExists('dob', 'students'); $hasRegistrationGrade = $this->db->fieldExists('registration_grade', 'students'); @@ -600,6 +603,14 @@ final class SchoolYearClosingService ->where('sc.school_year', $schoolYear) ->where('sc.class_section_id IS NOT NULL', null, false); + if ($hasEnrollments && ($hasEnrollmentStatus || $hasEnrollmentWithdrawn)) { + $builder->join( + 'enrollments e', + 'e.student_id = sc.student_id AND e.school_year = ' . $this->db->escape($schoolYear), + 'left' + ); + } + if ($hasDob) { $builder->select('s.dob'); } @@ -619,6 +630,19 @@ final class SchoolYearClosingService $builder->where('s.is_active', 1); } + if ($hasEnrollmentStatus) { + $inactiveStatuses = implode(',', array_map([$this->db, 'escape'], EnrollmentStatusService::INACTIVE_STATUSES)); + $builder->where( + '(e.enrollment_status IS NULL OR LOWER(TRIM(e.enrollment_status)) NOT IN (' . $inactiveStatuses . '))', + null, + false + ); + } + + if ($hasEnrollmentWithdrawn) { + $builder->where('(e.is_withdrawn IS NULL OR e.is_withdrawn != 1)', null, false); + } + $assignmentRows = $builder ->orderBy('sc.student_id', 'ASC') ->orderBy('sc.updated_at', 'DESC') diff --git a/app/Views/parent/enroll_classes.php b/app/Views/parent/enroll_classes.php index 1b2f59d..8803a43 100644 --- a/app/Views/parent/enroll_classes.php +++ b/app/Views/parent/enroll_classes.php @@ -345,7 +345,70 @@ foreach (($students ?? []) as $student) {
Review student information
-
Select each student you want to enroll for and update their information if needed.
+
Select each student you want to enroll for . Photo consent, medical conditions, and allergies can be updated below.
+
ucfirst(strtolower($m[0])), (string) $section) : 'Not Assigned'; + $medicalSelected = is_array($student['medical_conditions'] ?? null) + ? array_values(array_filter(array_map('trim', $student['medical_conditions']))) + : array_values(array_filter(array_map('trim', preg_split('/[,\n;]+/', (string) ($student['medical_conditions'] ?? ''), -1, PREG_SPLIT_NO_EMPTY) ?: []))); + $allergySelected = is_array($student['allergies'] ?? null) + ? array_values(array_filter(array_map('trim', $student['allergies']))) + : array_values(array_filter(array_map('trim', preg_split('/[,\n;]+/', (string) ($student['allergies'] ?? ''), -1, PREG_SPLIT_NO_EMPTY) ?: []))); + $medicalOther = implode(', ', array_diff($medicalSelected, $enrollmentMedicalOptions)); + $allergyOther = implode(', ', array_diff($allergySelected, $enrollmentAllergyOptions)); + if ($medicalOther !== '') { + $medicalSelected[] = 'Other'; + } + if ($allergyOther !== '') { + $allergySelected[] = 'Other'; + } + $dobDisplay = !empty($student['dob']) ? local_date($student['dob'], 'm-d-Y') : 'N/A'; + $photoConsentValue = (string) ($student['photo_consent'] ?? ''); + if ($photoConsentValue !== '0' && $photoConsentValue !== '1') { + $photoConsentValue = !empty($student['photo_consent']) ? '1' : ''; + } ?>
-
Current grade: · School ID:
+ +
+
School ID:
+
Date of birth: · Age:
+
Gender:
+
Registration grade:
+
Current grade:
+
Expected placement:
+
Required action:
+
+ -
-
- - -
-
- - + + + + + + +
+
+
Editable health information
- - -
-
- - -
-
- - -
-
-
- -
+
+ +
+ + +
+ disabled data-student-edit-input data-health-option=""> + +
+ +
+ +
+
+ +
+ + +
+ disabled data-student-edit-input data-health-option=""> + +
+ +
+
- -
- Date of birth: - - · Age: - -
-
Expected placement:
-
Required action:
+
@@ -462,9 +553,6 @@ foreach (($students ?? []) as $student) {
Previous-year carry-over balance:
Total currently due:
- -
-
Family account information is not available. Please contact the administration if you have questions about tuition or balance.
@@ -476,7 +564,7 @@ foreach (($students ?? []) as $student) {
Review the enrollment summary before submitting.
-
Fees by student
+
Summary
@@ -509,11 +597,8 @@ foreach (($students ?? []) as $student) {
Important information
    -
  • Submitting sends the selected enrollment(s) to admission review.
  • +
  • For newly registered students, enrollment status changes to admission under review after submitting the request.
  • Payments are processed on the first day of school.
  • - -
  • -
  • Students with a make-up exam decision may initially remain in the same grade until the make-up exam result is confirmed.
  • @@ -721,8 +806,118 @@ foreach (($students ?? []) as $student) { card.querySelectorAll('[data-student-edit-input]').forEach(field => { field.disabled = !enabled; }); + syncHealthGroup(card, 'medical_conditions'); + syncHealthGroup(card, 'allergies'); } + function syncHealthGroup(card, groupName) { + const selected = !!card.querySelector('[data-enroll-input]')?.checked; + const group = card.querySelector('[data-health-group="' + groupName + '"]'); + const otherInput = card.querySelector('[data-other-input="' + groupName + '"]'); + if (!group) { + return; + } + + const noneChecked = !!group.querySelector('[data-health-option="none"]:checked'); + const otherChecked = !!group.querySelector('[data-health-option="other"]:checked'); + + group.querySelectorAll('input[type="checkbox"]').forEach(input => { + if (!selected) { + input.disabled = true; + return; + } + + if (input.dataset.healthOption === 'none') { + input.disabled = false; + return; + } + + if (noneChecked) { + input.checked = false; + input.disabled = true; + return; + } + + input.disabled = false; + }); + + if (otherInput) { + otherInput.classList.toggle('d-none', !otherChecked); + otherInput.disabled = !selected || !otherChecked; + if (!otherChecked) { + otherInput.value = ''; + } + } + } + + function validateSelectedStudentHealth() { + for (const input of selectedEnrollInputs()) { + const card = input.closest('[data-student-card]'); + if (!card) { + continue; + } + const name = card.querySelector('.fw-semibold')?.textContent?.trim() || 'Student'; + const photo = card.querySelector("select[name$='[photo_consent]']"); + if (!photo || photo.value === '') { + alert('Please select photo consent for ' + name + '.'); + return false; + } + + const medicalChecked = card.querySelectorAll('[data-health-group="medical_conditions"] input[type="checkbox"]:checked'); + if (!medicalChecked.length) { + alert('Please select medical conditions for ' + name + '.'); + return false; + } + const medicalOther = card.querySelector('[data-other-input="medical_conditions"]'); + if (card.querySelector('[data-health-group="medical_conditions"] [data-health-option="other"]:checked') && (!medicalOther || !medicalOther.value.trim())) { + alert('Please specify the other medical condition for ' + name + '.'); + return false; + } + + const allergyChecked = card.querySelectorAll('[data-health-group="allergies"] input[type="checkbox"]:checked'); + if (!allergyChecked.length) { + alert('Please select allergies for ' + name + '.'); + return false; + } + const allergyOther = card.querySelector('[data-other-input="allergies"]'); + if (card.querySelector('[data-health-group="allergies"] [data-health-option="other"]:checked') && (!allergyOther || !allergyOther.value.trim())) { + alert('Please specify the other allergy for ' + name + '.'); + return false; + } + } + return true; + } + + document.querySelectorAll('[data-student-card]').forEach(card => { + card.querySelectorAll('[data-health-group]').forEach(group => { + group.addEventListener('change', function(event) { + const target = event.target; + if (!(target instanceof HTMLInputElement) || target.type !== 'checkbox') { + return; + } + const groupName = group.getAttribute('data-health-group'); + if (!groupName) { + return; + } + + if (target.dataset.healthOption === 'none' && target.checked) { + group.querySelectorAll('[data-health-option="item"], [data-health-option="other"]').forEach(input => { + input.checked = false; + }); + } + + if ((target.dataset.healthOption === 'item' || target.dataset.healthOption === 'other') && target.checked) { + const noneInput = group.querySelector('[data-health-option="none"]'); + if (noneInput) { + noneInput.checked = false; + } + } + + syncHealthGroup(card, groupName); + }); + }); + }); + if (startButton && flowModal) { startButton.addEventListener('click', function() { if (deadlinePassed) { @@ -768,6 +963,9 @@ foreach (($students ?? []) as $student) { alert('Please select at least one student to enroll.'); return; } + if (currentStep === 0 && !validateSelectedStudentHealth()) { + return; + } if (currentStep === 1) { syncPolicyAccepted(); if (!hasAcceptedSchoolPolicy) {