diff --git a/app/Config/Services.php b/app/Config/Services.php index 216c094..ca33655 100644 --- a/app/Config/Services.php +++ b/app/Config/Services.php @@ -311,7 +311,8 @@ class Services extends BaseService return new \App\Services\EnrollmentRegistrationEmailService( \Config\Database::connect(), static::enrollmentTransition(), - static::emailService() + static::emailService(), + model(\App\Models\ConfigurationModel::class) ); } diff --git a/app/Controllers/View/EnrollmentAdminController.php b/app/Controllers/View/EnrollmentAdminController.php index 471d695..aceeb8a 100644 --- a/app/Controllers/View/EnrollmentAdminController.php +++ b/app/Controllers/View/EnrollmentAdminController.php @@ -46,7 +46,6 @@ class EnrollmentAdminController extends BaseController 'schoolYears' => $this->schoolYears(), 'classSections' => $this->classSections($schoolYear), 'admins' => $this->adminUsers(), - 'enrollmentFollowups' => $this->enrollmentFollowups($schoolYear, $openFlags), 'auditRows' => $this->auditRows($schoolYear), 'activeExceptions' => $canManageExceptions ? $this->enrollmentExceptions($schoolYear) : [], 'exceptionNeeded' => $this->exceptionNeededFromFlags($openFlags), diff --git a/app/Controllers/View/StudentController.php b/app/Controllers/View/StudentController.php index a2338c5..365703e 100644 --- a/app/Controllers/View/StudentController.php +++ b/app/Controllers/View/StudentController.php @@ -1115,7 +1115,7 @@ class StudentController extends BaseController $builder->where('is_active', 1); } - $this->applyDistributionAgeFilter($builder, $year); + $this->applyDistributionAgeFilter($builder, $year, 'dob'); $rows = array_merge($rows, $builder->get()->getResultArray()); } @@ -1326,7 +1326,7 @@ class StudentController extends BaseController $builder->where('is_active', 1); } - $this->applyDistributionAgeFilter($builder, $year); + $this->applyDistributionAgeFilter($builder, $year, 'dob'); $rows = $builder ->orderBy('lastname', 'ASC') ->orderBy('firstname', 'ASC') @@ -2138,14 +2138,14 @@ class StudentController extends BaseController return $out; } - private function applyDistributionAgeFilter($builder, string $schoolYear): void + private function applyDistributionAgeFilter($builder, string $schoolYear, string $dobColumn = 'students.dob'): void { [$earliestDob, $latestDob] = $this->distributionAgeBirthDateWindow($schoolYear); $builder - ->where('students.dob IS NOT NULL', null, false) - ->where('students.dob >=', $earliestDob) - ->where('students.dob <=', $latestDob); + ->where($dobColumn . ' IS NOT NULL', null, false) + ->where($dobColumn . ' >=', $earliestDob) + ->where($dobColumn . ' <=', $latestDob); } private function distributionAgeBirthDateWindow(string $schoolYear): array diff --git a/app/Services/EnrollmentRegistrationEmailService.php b/app/Services/EnrollmentRegistrationEmailService.php index d661ff7..a3467f6 100644 --- a/app/Services/EnrollmentRegistrationEmailService.php +++ b/app/Services/EnrollmentRegistrationEmailService.php @@ -2,18 +2,20 @@ namespace App\Services; +use App\Models\ConfigurationModel; use App\Support\Enrollment\DeliberationDecision; use CodeIgniter\Database\BaseConnection; use DateTimeInterface; final class EnrollmentRegistrationEmailService { - public const TEMPLATE_VERSION = 'phase5_consolidated_v2'; + public const TEMPLATE_VERSION = 'phase5_consolidated_v3'; public function __construct( private readonly BaseConnection $db, private readonly EnrollmentTransitionService $transitionService, private readonly EmailService $emailService, + private readonly ?ConfigurationModel $configurationModel = null, ) { } @@ -151,6 +153,7 @@ final class EnrollmentRegistrationEmailService $opens = $this->dateText($schoolYear['registration_opens_at'] ?? $schoolYear['registration_starts_on'] ?? null); $studentRows = []; $studentIds = []; + $hasReEnrollmentEligibleStudent = false; foreach ($family['students'] as $student) { $studentId = (int) ($student['id'] ?? 0); @@ -161,18 +164,16 @@ final class EnrollmentRegistrationEmailService $evaluation = $this->transitionService->evaluate($studentId, $previousYear, $schoolYearName, 'parent'); $studentRows[] = $this->studentRow($student, $evaluation, $opens, $deadline); $studentIds[] = $studentId; + $hasReEnrollmentEligibleStudent = $hasReEnrollmentEligibleStudent || $this->canCompleteParentReEnrollment($evaluation); } $financial = $this->financialSection((int) $family['parent_user_id'], $schoolYear, $previousYear); $subject = 'Registration for ' . $schoolYearName . ' Is Now Open'; $bodyHtml = '

Dear ' . esc($family['name']) . ',

' - . '

Registration for the ' . esc($schoolYearName) . ' school year is now open. Please review the student summary below and complete the portal steps before ' . esc($deadline) . '.

' + . '

Registration for the ' . esc($schoolYearName) . ' school year is now open. Please review the student(s) summary below and complete the portal steps before ' . esc($deadline) . '.

' . $this->studentsTable($studentRows) . $financial - . '

Registration Steps

' - . '

Re-enrollment opens on ' . esc($opens) . ' and closes on ' . esc($deadline) . '. Complete re-enrollment for each eligible student before the deadline to secure their enrollment for the upcoming school year.

' - . '
  1. Sign in to the parent portal.
  2. Review student information and upload any required documents.
  3. Acknowledge school policies.
  4. Review tuition, fees, and any carry-over balance.
  5. Submit Enrollment.
' - . '

Registration portal: ' . esc(site_url('parent/enroll_classes')) . '

' + . $this->registrationStepsSection($opens, $deadline, $hasReEnrollmentEligibleStudent) . '

For assistance, please contact the school administration.

' . '

Sincerely,
Al Rahma Sunday School
School Administration

'; @@ -189,16 +190,32 @@ final class EnrollmentRegistrationEmailService ]; } + private function canCompleteParentReEnrollment(array $evaluation): bool + { + return (bool) ($evaluation['parent_enrollment_allowed'] ?? false); + } + + private function registrationStepsSection(string $opens, string $deadline, bool $include): string + { + if (! $include) { + return ''; + } + + return '

Re-enrollment Steps

' + . '

Re-enrollment opens on ' . esc($opens) . ' and closes on ' . esc($deadline) . '. Complete re-enrollment for each eligible student before the deadline to secure their enrollment for the upcoming school year.

' + . '
  1. Sign in to the parent portal.
  2. Review/update student and contact information.
  3. Acknowledge school policies.
  4. Review tuition, fees and any carry-over balance.
  5. Submit Enrollment.
' + . '

Registration portal: ' . esc(site_url('parent/enroll_classes')) . '

'; + } + private function studentsTable(array $studentRows): string { if ($studentRows === []) { return '

No eligible linked students were found for this email.

'; } - return '' - . '' + return '
' . implode('', $studentRows) - . '
'; + . ''; } private function studentRow(array $student, array $evaluation, string $opens, string $deadline): string @@ -207,17 +224,52 @@ final class EnrollmentRegistrationEmailService $decision = DeliberationDecision::display((string) ($evaluation['deliberation_decision'] ?? '')); $status = $this->registrationStatus($evaluation); $placement = $this->placementText($evaluation); - $requiredAction = $this->requiredAction($evaluation, $deadline); + $requiredAction = $this->requiredAction($evaluation, $deadline, $name); $message = $this->decisionMessage($name, $evaluation, $opens, $deadline); + $nextStepHtml = $this->decisionMessageHtml($message, (string) ($evaluation['deliberation_decision'] ?? '')); + if ($requiredAction !== '') { + $nextStepHtml .= '
' . esc($requiredAction) . ''; + } + return '' + . '' + . '' + . '' + . '' + . $this->studentDetailRow('Decision', esc($decision ?: 'Pending')) + . $this->studentDetailRow('Registration Status', '' . esc($status) . '
' . esc($placement)) + . $this->studentDetailRow('Next Step', $nextStepHtml) + . '
' + . '' . esc($name) . '' + . '
'; + } + + private function studentDetailRow(string $label, string $valueHtml): string + { return '' - . '' . esc($name) . '' - . '' . esc($decision ?: 'Pending') . '' - . '' . esc($status) . '
' . esc($placement) . '' - . '' . esc($message) . '
' . esc($requiredAction) . '' + . '' + . '
' . esc($label) . '
' + . '
' . $valueHtml . '
' + . '' . ''; } + private function decisionMessageHtml(string $message, string $decision): string + { + if ($decision !== DeliberationDecision::MAKE_UP_EXAM) { + return nl2br(esc($message), false); + } + + $lines = explode("\n", $message); + $lastIndex = count($lines) - 1; + foreach ($lines as $index => $line) { + $escaped = esc($line); + $lines[$index] = $index === $lastIndex ? '' . $escaped . '' : $escaped; + } + + return implode('
', $lines); + } + private function decisionMessage(string $name, array $evaluation, string $opens, string $deadline): string { if ((string) ($evaluation['deliberation_decision'] ?? '') === DeliberationDecision::PASSED) { @@ -231,12 +283,29 @@ final class EnrollmentRegistrationEmailService } return match ((string) ($evaluation['deliberation_decision'] ?? '')) { - DeliberationDecision::REPEAT_CLASS => 'The deliberation decision for ' . $name . ' is to repeat the current grade. After registration is completed, the student will remain in the same grade and class when available.', - DeliberationDecision::MAKE_UP_EXAM => 'The deliberation decision for ' . $name . ' is pending the result of a make-up exam. Registration may be completed now. The student will initially remain in the same grade.', + DeliberationDecision::REPEAT_CLASS => 'The deliberation decision for ' . $name . ' is to repeat the current grade. After re-enrollment is completed, the student will remain in the same grade.', + DeliberationDecision::MAKE_UP_EXAM => 'The final academic decision for ' . $name . ' is currently pending the result of a make-up exam. This exam is scheduled for ' . $this->makeupExamDateText() . ' from 9:30 AM to 11:00 AM at ISGL.' + . "\n" . 'The exam result will determine whether the student advances to the next grade or repeats the current class. Failure to attend the make-up exam will automatically result in the student repeating the class, as no further retake opportunities will be available.' + . "\n" . 'You must re-enroll ' . $name . ' before the make-up exam can be taken.', default => 'Please review the registration portal for the current enrollment status.', }; } + private function makeupExamDateText(): string + { + $raw = trim((string) (($this->configurationModel ?? new ConfigurationModel())->getConfig('make-up-exam') ?? '')); + if ($raw === '') { + return 'a date to be announced'; + } + + $timestamp = strtotime($raw); + if ($timestamp !== false) { + return date('m-d-Y', $timestamp); + } + + return $raw; + } + private function financialSection(int $parentId, array $schoolYear, ?string $previousYear): string { $carry = $previousYear !== null ? $this->invoiceBalance($parentId, $previousYear) : 0.0; @@ -250,14 +319,11 @@ final class EnrollmentRegistrationEmailService $message = trim((string) ($schoolYear['financial_policy_message'] ?? '')); if ($message === '') { - $message = 'The balance is shown for information and does not currently block registration.'; + $message = 'The balance needs to be settled with the school before the re-enrollment process can be started.'; } return '

Family Account Information

' . '

Carry-over balance: $' . number_format($carry, 2) . '
' - . 'Registration fee: $' . number_format($registrationFee, 2) . '
' - . 'New-year tuition due now: $' . number_format($tuition, 2) . '
' - . 'Mandatory fees: $' . number_format($mandatory, 2) . '
' . 'Total currently due: $' . number_format($total, 2) . '

' . '

' . esc($message) . '

'; } @@ -428,13 +494,13 @@ final class EnrollmentRegistrationEmailService 'automatic_distribution_pending' => $this->assignedGradeText($evaluation), 'same_class_assigned' => 'Same grade and class', 'temporary_same_grade' => 'Same grade initially', - 'manual_class_required' => 'Same grade - administrative class assignment required', + 'manual_class_required' => 'Same grade', 'exit_required' => 'Completion or exit process required', default => 'Pending', }; } - private function requiredAction(array $evaluation, string $deadline): string + private function requiredAction(array $evaluation, string $deadline, string $name = 'The student'): string { if ((string) ($evaluation['deliberation_decision'] ?? '') === DeliberationDecision::PASSED) { return 'Complete re-enrollment before ' . $deadline . '.'; @@ -444,7 +510,7 @@ final class EnrollmentRegistrationEmailService return ($evaluation['adult_student'] ?? false) ? 'Student must complete the authorized adult-student process or contact administration.' : 'Contact the school administration.'; } return ($evaluation['deliberation_decision'] ?? '') === DeliberationDecision::MAKE_UP_EXAM - ? 'Complete re-enrollment and follow the school instructions regarding the make-up exam.' + ? '' : 'Complete re-enrollment before ' . $deadline . '.'; } diff --git a/app/Views/about.php b/app/Views/about.php index 4268f45..b5927e1 100644 --- a/app/Views/about.php +++ b/app/Views/about.php @@ -44,7 +44,7 @@ \ No newline at end of file + diff --git a/app/Views/partials/header_back.php b/app/Views/partials/header_back.php index 393d4aa..93e66a8 100644 --- a/app/Views/partials/header_back.php +++ b/app/Views/partials/header_back.php @@ -56,7 +56,7 @@
- School Icon + School Icon School Management Dashboard diff --git a/app/Views/success.php b/app/Views/success.php index 86c6538..7c31adb 100644 --- a/app/Views/success.php +++ b/app/Views/success.php @@ -5,7 +5,7 @@
@@ -28,4 +28,4 @@
-endSection() ?> \ No newline at end of file +endSection() ?> diff --git a/app/Views/teacher/teacher_contactus.php b/app/Views/teacher/teacher_contactus.php index 81f979a..34f5b02 100644 --- a/app/Views/teacher/teacher_contactus.php +++ b/app/Views/teacher/teacher_contactus.php @@ -43,7 +43,7 @@