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.
'
- . 'Sign in to the parent portal. Review student information and upload any required documents. Acknowledge school policies. Review tuition, fees, and any carry-over balance. 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.
'
+ . 'Sign in to the parent portal. Review/update student and contact information. Acknowledge school policies. Review tuition, fees and any carry-over balance. 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 ''
+ . ''
+ . ''
+ . ''
+ . '' . esc($name) . ' '
+ . ' '
+ . ' '
+ . $this->studentDetailRow('Decision', esc($decision ?: 'Pending'))
+ . $this->studentDetailRow('Registration Status', '' . esc($status) . ' ' . esc($placement))
+ . $this->studentDetailRow('Next Step', $nextStepHtml)
+ . '
';
+ }
+
+ 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 @@
-
+
Al Rahma Sunday School
diff --git a/app/Views/administrator/enrollment_admin_dashboard.php b/app/Views/administrator/enrollment_admin_dashboard.php
index 80f33c9..7a05cb2 100644
--- a/app/Views/administrator/enrollment_admin_dashboard.php
+++ b/app/Views/administrator/enrollment_admin_dashboard.php
@@ -222,7 +222,6 @@ $nonOverridableCodes = ['STUDENT_NOT_LINKED', 'SOURCE_YEAR_NOT_FOUND', 'TARGET_Y
$previewStudents = is_array($preview['students'] ?? null) ? $preview['students'] : [];
$hasGrantableStudent = false;
$openFlagCount = (int) ($openFlagCount ?? 0);
-$followupCount = count($enrollmentFollowups ?? []);
$activeExceptionCount = 0;
foreach (($activeExceptions ?? []) as $exceptionRow) {
if (($exceptionRow['status'] ?? '') === 'active') {
@@ -273,15 +272,9 @@ $reasonCodes = is_array($exceptionReasonCodes ?? null) ? $exceptionReasonCodes :
= $openFlagCount ?>
-
-
- 2. Follow-up
- = $followupCount ?>
-
-
- 3. Parent emails
+ 2. Parent emails
= esc($emailStepLabel) ?>
@@ -348,8 +341,7 @@ $reasonCodes = is_array($exceptionReasonCodes ?? null) ? $exceptionReasonCodes :
School ID
Issue
Details
- Assigned
- Created
+ Update by
What to do
@@ -380,7 +372,6 @@ $reasonCodes = is_array($exceptionReasonCodes ?? null) ? $exceptionReasonCodes :
= esc($flag['assignee_name'] ?: '') ?>
- = esc(!empty($flag['created_at']) ? local_datetime($flag['created_at'], 'm-d-Y H:i') : '') ?>
@@ -435,67 +426,6 @@ $reasonCodes = is_array($exceptionReasonCodes ?? null) ? $exceptionReasonCodes :
-
-
Enrollments still waiting on staff
-
These students already have an enrollment record. If a class or exception is needed, handle it in Work queue.
-
-
-
-
-
Family exception tools
Use the lookup to grant a family exception after reviewing the matching work queue item. Saved exceptions stay here for audit and revocation.
@@ -904,7 +834,6 @@ $reasonCodes = is_array($exceptionReasonCodes ?? null) ? $exceptionReasonCodes :
var tableMessages = {
enrollmentEmailExamplesTable: 'No parent decision email examples found for this school year.',
- enrollmentFollowupTable: 'No enrollment records need follow-up for this school year.',
enrollmentFlagsTable: 'No students with enrollment blockages were found for these filters.',
enrollmentFamilyEligibilityTable: 'No linked students found for this parent.',
enrollmentExceptionsTable: 'No enrollment exceptions found for this school year.',
@@ -945,11 +874,27 @@ $reasonCodes = is_array($exceptionReasonCodes ?? null) ? $exceptionReasonCodes :
});
}
+ function activateTabFromHash() {
+ if (!window.bootstrap || !window.bootstrap.Tab || !window.location.hash) {
+ return;
+ }
+
+ var target = window.location.hash;
+ var trigger = document.querySelector('[data-bs-toggle="tab"][data-bs-target="' + target + '"]');
+ if (trigger) {
+ window.bootstrap.Tab.getOrCreateInstance(trigger).show();
+ }
+ }
+
mergeExceptionPaneIntoWorkQueue();
if (document.readyState === 'loading') {
- document.addEventListener('DOMContentLoaded', initEnrollmentAdminTables);
+ document.addEventListener('DOMContentLoaded', function () {
+ activateTabFromHash();
+ initEnrollmentAdminTables();
+ });
} else {
+ activateTabFromHash();
initEnrollmentAdminTables();
}
})();
diff --git a/app/Views/administrator/enrollment_email_preview.php b/app/Views/administrator/enrollment_email_preview.php
index bcbd8be..04fdcd3 100644
--- a/app/Views/administrator/enrollment_email_preview.php
+++ b/app/Views/administrator/enrollment_email_preview.php
@@ -3,8 +3,8 @@
@@ -14,7 +14,7 @@
Registration Email Preview
= esc($schoolYear ?? '') ?> ยท Parent #= esc((string) ($parentId ?? '')) ?>
- Back
+ Back
diff --git a/app/Views/appointment.php b/app/Views/appointment.php
index a3483e2..004da15 100644
--- a/app/Views/appointment.php
+++ b/app/Views/appointment.php
@@ -48,7 +48,7 @@
-
+
Al Rahma Sunday School
diff --git a/app/Views/auth/select_role.php b/app/Views/auth/select_role.php
index c0634e2..25e2784 100644
--- a/app/Views/auth/select_role.php
+++ b/app/Views/auth/select_role.php
@@ -14,7 +14,7 @@
= csrf_field() ?>
diff --git a/app/Views/call_to_action.php b/app/Views/call_to_action.php
index 8422576..21d6403 100644
--- a/app/Views/call_to_action.php
+++ b/app/Views/call_to_action.php
@@ -48,7 +48,7 @@
-
+
Al Rahma Sunday School
diff --git a/app/Views/certificates/verify.php b/app/Views/certificates/verify.php
index f24891f..3707feb 100644
--- a/app/Views/certificates/verify.php
+++ b/app/Views/certificates/verify.php
@@ -11,7 +11,7 @@
.verify-card { max-width: 520px; margin: 60px auto; }
.badge-valid { background: #198754; }
.badge-invalid { background: #dc3545; }
- .cert-logo { width: 70px; height: 70px; border-radius: 50%; object-fit: cover; }
+ .cert-logo { width: 70px; height: 70px; border-radius: 50%; object-fit: contain; background-color: #fff; }
.field-label { font-size: .8rem; text-transform: uppercase; letter-spacing: .05em; color: #6c757d; }
.field-value { font-size: 1.05rem; font-weight: 500; }
diff --git a/app/Views/classes.php b/app/Views/classes.php
index 9e22d1b..2118a39 100644
--- a/app/Views/classes.php
+++ b/app/Views/classes.php
@@ -45,7 +45,7 @@
-
+
Al Rahma Sunday School
diff --git a/app/Views/contact.php b/app/Views/contact.php
index 07ce3e3..2e06e9d 100644
--- a/app/Views/contact.php
+++ b/app/Views/contact.php
@@ -44,7 +44,7 @@
-
+
Al Rahma Sunday School
diff --git a/app/Views/errors/custom/blocked.php b/app/Views/errors/custom/blocked.php
index 6a362a0..65218de 100644
--- a/app/Views/errors/custom/blocked.php
+++ b/app/Views/errors/custom/blocked.php
@@ -4,7 +4,7 @@
+ style="width: 120px; height: 120px; border-radius: 50%; object-fit: contain; background-color: #fff;" class="mb-4">
Access Blocked
Too many password reset attempts have been made.
Please try again after 24 hours or contact support if you need urgent assistance.
diff --git a/app/Views/errors/invalid_token.php b/app/Views/errors/invalid_token.php
index d26941b..80e4d56 100644
--- a/app/Views/errors/invalid_token.php
+++ b/app/Views/errors/invalid_token.php
@@ -5,7 +5,7 @@
-= $this->endSection() ?>
\ No newline at end of file
+= $this->endSection() ?>
diff --git a/app/Views/facility.php b/app/Views/facility.php
index f0be3f4..0ff707f 100644
--- a/app/Views/facility.php
+++ b/app/Views/facility.php
@@ -44,7 +44,7 @@
-
+
Al Rahma Sunday School
@@ -88,4 +88,4 @@