fix is_active student flag logic and make moving with enrollment status
Deploy to Shared Hosting / Shared hosting deploy (push) Failing after 47s
Tests / PHPUnit (push) Successful in 1m16s

This commit is contained in:
root
2026-08-20 03:11:37 -04:00
parent 6f722795c5
commit 0a31aa1393
20 changed files with 588 additions and 489 deletions
+171
View File
@@ -0,0 +1,171 @@
<?php
namespace App\Commands;
use App\Services\EnrollmentStatusService;
use CodeIgniter\CLI\BaseCommand;
use CodeIgniter\CLI\CLI;
use CodeIgniter\Database\BaseConnection;
class SyncStudentActivity extends BaseCommand
{
protected $group = 'Enrollment';
protected $name = 'students:sync-activity';
protected $description = 'Synchronize students.is_active from current-year enrollment_status.';
protected $usage = 'php spark students:sync-activity --school-year=YYYY-YYYY [--dry-run]';
protected $options = [
'--school-year' => 'School year to use as the controlling enrollment year.',
'--dry-run' => 'Report changes without updating students.',
];
private BaseConnection $db;
private EnrollmentStatusService $statusService;
public function run(array $params)
{
$this->db = \Config\Database::connect();
$this->statusService = new EnrollmentStatusService($this->db);
$schoolYear = $this->optionValue('school-year');
if ($schoolYear === '') {
$schoolYear = $this->statusService->currentSchoolYear();
}
if ($schoolYear === '') {
CLI::error('Missing --school-year and no configured current school year was found.');
return EXIT_ERROR;
}
$dryRun = CLI::getOption('dry-run') !== null;
$duplicates = $this->duplicates($schoolYear);
$unknownStatuses = [];
$withoutEnrollment = [];
$mismatches = [];
$updated = 0;
$students = $this->db->table('students')
->select('id, school_id, firstname, lastname, is_active')
->orderBy('id', 'ASC')
->get()
->getResultArray();
foreach ($students as $student) {
$studentId = (int) $student['id'];
$enrollment = $this->statusService->controllingEnrollment($studentId, $schoolYear);
if ($enrollment === null) {
$expected = 0;
$withoutEnrollment[] = $this->studentLabel($student);
} else {
try {
$status = $this->statusService->normalizeStatus((string) ($enrollment['enrollment_status'] ?? ''));
$expected = $this->statusService->activeFlagForStatus($status);
} catch (\InvalidArgumentException $e) {
$unknownStatuses[] = [
'student' => $this->studentLabel($student),
'enrollment_id' => (int) ($enrollment['id'] ?? 0),
'status' => (string) ($enrollment['enrollment_status'] ?? ''),
];
continue;
}
}
$actual = (int) ($student['is_active'] ?? 1);
if ($actual !== $expected) {
$mismatches[] = [
'student' => $this->studentLabel($student),
'current' => $actual,
'expected' => $expected,
'status' => (string) ($enrollment['enrollment_status'] ?? 'no enrollment'),
];
if (! $dryRun) {
$this->db->table('students')->where('id', $studentId)->update(['is_active' => $expected]);
$updated++;
}
}
}
CLI::write('School year: ' . $schoolYear);
CLI::write('Dry run: ' . ($dryRun ? 'yes' : 'no'));
CLI::write('Duplicate enrollment groups: ' . count($duplicates));
foreach ($duplicates as $row) {
CLI::write(sprintf(
' student_id=%d school_year=%s semester=%s count=%d',
(int) $row['student_id'],
(string) $row['school_year'],
(string) ($row['semester'] ?? ''),
(int) $row['row_count']
));
}
CLI::write('Unknown statuses: ' . count($unknownStatuses));
foreach ($unknownStatuses as $row) {
CLI::write(sprintf(' %s enrollment_id=%d status=%s', $row['student'], $row['enrollment_id'], $row['status']));
}
CLI::write('Students without current enrollment: ' . count($withoutEnrollment));
foreach (array_slice($withoutEnrollment, 0, 50) as $label) {
CLI::write(' ' . $label);
}
if (count($withoutEnrollment) > 50) {
CLI::write(' ... ' . (count($withoutEnrollment) - 50) . ' more');
}
CLI::write('Mismatches: ' . count($mismatches));
foreach ($mismatches as $row) {
CLI::write(sprintf(
' %s current=%d expected=%d status=%s',
$row['student'],
$row['current'],
$row['expected'],
$row['status']
));
}
if (! $dryRun) {
CLI::write('Updated students: ' . $updated);
}
return EXIT_SUCCESS;
}
private function duplicates(string $schoolYear): array
{
return $this->db->table('enrollments')
->select('student_id, school_year, semester, COUNT(*) AS row_count')
->where('school_year', $schoolYear)
->groupBy('student_id, school_year, semester')
->having('COUNT(*) >', 1)
->orderBy('student_id', 'ASC')
->get()
->getResultArray();
}
private function studentLabel(array $student): string
{
$name = trim((string) ($student['firstname'] ?? '') . ' ' . (string) ($student['lastname'] ?? ''));
return '#' . (int) ($student['id'] ?? 0) . ($name !== '' ? ' ' . $name : '');
}
private function optionValue(string $name): string
{
$value = CLI::getOption($name);
if (is_string($value) && trim($value) !== '') {
return trim($value);
}
$argv = $_SERVER['argv'] ?? [];
$prefix = '--' . $name . '=';
foreach ($argv as $index => $arg) {
if (is_string($arg) && str_starts_with($arg, $prefix)) {
return trim(substr($arg, strlen($prefix)));
}
if ($arg === '--' . $name && isset($argv[$index + 1])) {
return trim((string) $argv[$index + 1]);
}
}
return '';
}
}
+11 -12
View File
@@ -107,16 +107,17 @@ $routes->post('student/score-card', 'View\StudentController::scoreCard');
$routes->get('student/score-card', 'View\StudentController::scoreCardIndex');
$routes->get('student/score-card/list', 'View\StudentController::scoreCardList');
$routes->get('administrator/student-score-card', 'View\StudentController::scoreCardAdmin');
$routes->get('administrator/enrollment-admin', 'View\EnrollmentAdminController::dashboard', ['filter' => 'auth:admin']);
$routes->get('administrator/enrollment-admin/email-preview', 'View\EnrollmentAdminController::previewEmail', ['filter' => 'auth:admin']);
$routes->post('administrator/enrollment-admin/approve-launch', 'View\EnrollmentAdminController::approveLaunch', ['filter' => 'auth:admin']);
$routes->post('administrator/enrollment-admin/send-registration-emails', 'View\EnrollmentAdminController::sendRegistrationEmails', ['filter' => 'auth:admin']);
$routes->post('administrator/enrollment-admin/flags/(:num)/resolve', 'View\EnrollmentAdminController::resolveFlag/$1', ['filter' => 'auth:admin']);
$routes->post('administrator/enrollment-admin/flags/(:num)/assign-class', 'View\EnrollmentAdminController::assignClass/$1', ['filter' => 'auth:admin']);
$routes->post('administrator/enrollment-admin/flags/(:num)/makeup-promotion', 'View\EnrollmentAdminController::confirmMakeupPromotion/$1', ['filter' => 'auth:admin']);
$routes->post('administrator/enrollment-admin/flags/(:num)/approve-exception', 'View\EnrollmentAdminController::approveException/$1', ['filter' => 'auth:admin']);
$routes->post('administrator/enrollment-admin/exceptions/create', 'View\EnrollmentAdminController::createException', ['filter' => 'auth:admin']);
$routes->post('administrator/enrollment-admin/exceptions/(:num)/revoke', 'View\EnrollmentAdminController::revokeException/$1', ['filter' => 'auth:admin']);
$enrollmentAdminFilter = 'auth:admin|administrator|principal|vice_principal|vice principal';
$routes->get('administrator/enrollment-admin', 'View\EnrollmentAdminController::dashboard', ['filter' => $enrollmentAdminFilter]);
$routes->get('administrator/enrollment-admin/email-preview', 'View\EnrollmentAdminController::previewEmail', ['filter' => $enrollmentAdminFilter]);
$routes->post('administrator/enrollment-admin/approve-launch', 'View\EnrollmentAdminController::approveLaunch', ['filter' => $enrollmentAdminFilter]);
$routes->post('administrator/enrollment-admin/send-registration-emails', 'View\EnrollmentAdminController::sendRegistrationEmails', ['filter' => $enrollmentAdminFilter]);
$routes->post('administrator/enrollment-admin/flags/(:num)/resolve', 'View\EnrollmentAdminController::resolveFlag/$1', ['filter' => $enrollmentAdminFilter]);
$routes->post('administrator/enrollment-admin/flags/(:num)/assign-class', 'View\EnrollmentAdminController::assignClass/$1', ['filter' => $enrollmentAdminFilter]);
$routes->post('administrator/enrollment-admin/flags/(:num)/makeup-promotion', 'View\EnrollmentAdminController::confirmMakeupPromotion/$1', ['filter' => $enrollmentAdminFilter]);
$routes->post('administrator/enrollment-admin/flags/(:num)/approve-exception', 'View\EnrollmentAdminController::approveException/$1', ['filter' => $enrollmentAdminFilter]);
$routes->post('administrator/enrollment-admin/exceptions/create', 'View\EnrollmentAdminController::createException', ['filter' => $enrollmentAdminFilter]);
$routes->post('administrator/enrollment-admin/exceptions/(:num)/revoke', 'View\EnrollmentAdminController::revokeException/$1', ['filter' => $enrollmentAdminFilter]);
$routes->get('administrator/financial-aid', 'Administrator\FinancialAidController::index', ['filter' => 'auth:admin|administrator|principal|oversee_financial_aid']);
$routes->get('administrator/financial-aid/(:num)', 'Administrator\FinancialAidController::show/$1', ['filter' => 'auth:admin|administrator|principal|oversee_financial_aid']);
$routes->post('administrator/financial-aid/(:num)/approve', 'Administrator\FinancialAidController::approve/$1', ['filter' => 'auth:admin|administrator|principal|oversee_financial_aid']);
@@ -774,7 +775,6 @@ $routes->get('administrator/userSearch', 'View\AdministratorController::userSear
$routes->get('/administrator/student_profiles', 'View\AdministratorController::studentProfiles');
$routes->get('/administrator/parent_profiles', 'View\AdministratorController::parentProfiles');
$routes->get('/administrator/removed_students', 'View\StudentController::removedStudents');
$routes->get('/administrator/teacher-submissions', 'View\AdministratorController::teacherSubmissionsReport', ['filter' => 'auth:admin']);
$routes->post('/administrator/teacher-submissions/notify', 'View\AdministratorController::sendTeacherSubmissionNotifications', ['filter' => 'auth:admin']);
$routes->get('/administrator/exam-drafts', 'View\ExamDraftController::adminIndex', ['filter' => 'auth:admin']);
@@ -1268,7 +1268,6 @@ $routes->get('/access_denied', 'ErrorController::accessDenied');
$routes->post('administrator/students/update', 'View\StudentController::editStudentData', ['filter' => 'auth:edit_student,update']);
$routes->post('administrator/students/set-active', 'View\StudentController::setStudentActive', ['filter' => 'auth:edit_student,update']);
/*
* --------------------------------------------------------------------
+9
View File
@@ -293,6 +293,15 @@ class Services extends BaseService
return new \App\Services\EnrollmentTransitionService(\Config\Database::connect());
}
public static function enrollmentStatus(bool $getShared = true): \App\Services\EnrollmentStatusService
{
if ($getShared) {
return static::getSharedInstance('enrollmentStatus');
}
return new \App\Services\EnrollmentStatusService(\Config\Database::connect());
}
public static function enrollmentRegistrationEmail(bool $getShared = true): \App\Services\EnrollmentRegistrationEmailService
{
if ($getShared) {
@@ -2656,13 +2656,16 @@ class AdministratorController extends BaseController
$payload = $this->filterEnrollmentPayloadByColumns($payload);
if ($existing !== null) {
$this->db->table('enrollments')
->where('id', (int) $existing['id'])
->update($payload);
$payload['id'] = (int) $existing['id'];
} else {
$payload['created_at'] = $now;
$this->db->table('enrollments')->insert($this->filterEnrollmentPayloadByColumns($payload));
}
\Config\Services::enrollmentStatus(false)->upsertStatus(
$this->filterEnrollmentPayloadByColumns($payload),
(int) (session()->get('user_id') ?? 0) ?: null,
'admin_review_decision_enrollment'
);
}
}
@@ -2961,6 +2964,8 @@ class AdministratorController extends BaseController
public function adminEnrollmentWithdrawalHandler()
{
$refundService = new FeeCalculationService();
$enrollmentStatusService = \Config\Services::enrollmentStatus(false);
$performedBy = (int) (session()->get('user_id') ?? 0) ?: null;
$this->db->transStart();
try {
@@ -3023,7 +3028,7 @@ class AdministratorController extends BaseController
$isWithdrawn = in_array($newEnrollmentStatus, ['withdrawn', 'refund pending', 'withdraw under review'], true) ? 1 : 0;
$ok = $this->db->table('enrollments')->insert([
$result = $enrollmentStatusService->upsertStatus([
'student_id' => (int)$studentId,
'parent_id' => $parentId,
'school_year' => (string)$this->schoolYear,
@@ -3034,9 +3039,9 @@ class AdministratorController extends BaseController
'admission_status' => $admissionStatus,
'created_at' => utc_now(),
'updated_at' => utc_now(),
]);
], $performedBy, 'admin_enrollment_withdrawal_handler');
if (!$ok) {
if ((int) ($result['id'] ?? 0) <= 0) {
$errors[] = "Failed to create enrollment for student ID $studentId.";
continue;
}
@@ -3080,26 +3085,36 @@ class AdministratorController extends BaseController
// admissionStatus computed above
// Skip if no actual change
if ($oldStatus === $newEnrollmentStatus) {
log_message('debug', "No status change for student {$studentId} ({$oldStatus}) — skipping.");
$enrollmentStatusService->upsertStatus([
'id' => (int) $enrollmentRow['id'],
'student_id' => (int) $studentId,
'parent_id' => (int) $parentId,
'school_year' => (string) $this->schoolYear,
'semester' => (string) ($enrollmentRow['semester'] ?? $this->semester),
'enrollment_status' => $newEnrollmentStatus,
'admission_status' => $admissionStatus,
'updated_at' => utc_now(),
], $performedBy, 'admin_enrollment_status_repair');
log_message('debug', "No status change for student {$studentId} ({$oldStatus}); repaired activity flag.");
if (in_array($newEnrollmentStatus, ['payment pending', 'enrolled'], true)) {
$this->applyDistributionDraftToStudentClass((int)$studentId, (string)$this->schoolYear);
}
continue;
}
// Update enrollment
$updated = $this->db->table('enrollments')
->where('student_id', $studentId)
->where('school_year', $this->schoolYear)
->update([
'enrollment_status' => $newEnrollmentStatus,
'admission_status' => $admissionStatus,
'updated_at' => utc_now(),
]);
$result = $enrollmentStatusService->upsertStatus([
'id' => (int) $enrollmentRow['id'],
'student_id' => (int) $studentId,
'parent_id' => (int) $parentId,
'school_year' => (string) $this->schoolYear,
'semester' => (string) ($enrollmentRow['semester'] ?? $this->semester),
'enrollment_status' => $newEnrollmentStatus,
'admission_status' => $admissionStatus,
'updated_at' => utc_now(),
], $performedBy, 'admin_enrollment_withdrawal_handler');
if (!$updated) {
if ((int) ($result['id'] ?? 0) <= 0) {
$errors[] = "Failed to update enrollment for student ID $studentId.";
continue;
}
+18 -10
View File
@@ -491,19 +491,27 @@ class DiscountController extends BaseController
return 0;
}
// 5) Perform update with same filters
$builder = $db->table('enrollments');
$builder->set('enrollment_status', 'enrolled')
// Use DB date if you prefer: ->set('enrollment_date', 'CURRENT_DATE()', false)
->set('enrollment_date', local_date(utc_now(), 'Y-m-d'))
->whereIn('id', $toUpdateIds);
$rowsToUpdate = $db->table('enrollments')
->whereIn('id', $toUpdateIds)
->get()
->getResultArray();
$statusService = \Config\Services::enrollmentStatus(false);
if ($builder->update() === false) {
$err = $db->error();
throw new \RuntimeException('Enrollments update failed: ' . ($err['message'] ?? 'unknown DB error'));
foreach ($rowsToUpdate as $row) {
$statusService->upsertStatus([
'id' => (int) $row['id'],
'student_id' => (int) $row['student_id'],
'parent_id' => (int) $row['parent_id'],
'school_year' => (string) $row['school_year'],
'semester' => (string) ($row['semester'] ?? ''),
'enrollment_date' => local_date(utc_now(), 'Y-m-d'),
'enrollment_status' => 'enrolled',
'admission_status' => 'accepted',
'updated_at' => utc_now(),
], (int) (session()->get('user_id') ?? 0) ?: null, 'discount_payment_completed');
}
$affected = $db->affectedRows();
$affected = count($rowsToUpdate);
$db->transCommit();
log_message(
+26 -7
View File
@@ -509,6 +509,7 @@ class ParentController extends BaseController
}
$this->db->transStart();
$enrollmentStatusService = \Config\Services::enrollmentStatus(false);
foreach ($enroll as $studentId) {
$studentId = (int) $studentId;
if (! isset($evaluations[$studentId])) {
@@ -548,7 +549,13 @@ class ParentController extends BaseController
if ($existingEnrollment['is_withdrawn'] == 1) {
// Reactivate the enrollment if the student was previously withdrawn
$this->enrollmentModel->update((int) $existingEnrollment['id'], $update);
$enrollmentStatusService->upsertStatus(array_merge($update, [
'id' => (int) $existingEnrollment['id'],
'student_id' => $studentId,
'parent_id' => $parentId,
'school_year' => $selectedYear,
'semester' => $this->semester,
]), (int) $parentId, 'parent_re_enrollment_submitted');
log_message('info', "{$studentData[$studentId]['firstname']} {$studentData[$studentId]['lastname']} (ID $studentId) has been re-enrolled in enrollment ID {$existingEnrollment['id']}.");
// Apply promotion-based class placement for the upcoming year
$this->applyPromotionAssignment((int)$studentId, $selectedYear, $isReturningReEnrollment);
@@ -557,7 +564,13 @@ class ParentController extends BaseController
if ($currentStatus === 'enrolled') {
$update['admission_status'] = 'accepted';
}
$this->enrollmentModel->update((int) $existingEnrollment['id'], $update);
$enrollmentStatusService->upsertStatus(array_merge($update, [
'id' => (int) $existingEnrollment['id'],
'student_id' => $studentId,
'parent_id' => $parentId,
'school_year' => $selectedYear,
'semester' => $this->semester,
]), (int) $parentId, 'parent_enrollment_submitted');
log_message('info', "{$studentData[$studentId]['firstname']} {$studentData[$studentId]['lastname']} (ID $studentId) is already actively enrolled.");
$this->applyPromotionAssignment((int)$studentId, $selectedYear, $isReturningReEnrollment);
}
@@ -592,9 +605,9 @@ class ParentController extends BaseController
'admission_status' => $targetAdmissionStatus,
'created_at' => utc_now()
]);
$result = $this->enrollmentModel->insert($payload, true);
$result = $enrollmentStatusService->upsertStatus($payload, (int) $parentId, 'parent_enrollment_submitted');
if (!$result) {
if ((int) ($result['id'] ?? 0) <= 0) {
$this->db->transRollback();
return redirect()->back()->withInput()->with('error', $studentName . ': Unable to save enrollment.');
} else {
@@ -603,7 +616,7 @@ class ParentController extends BaseController
$this->applyPromotionAssignment((int)$studentId, $selectedYear, $isReturningReEnrollment);
}
$enrollmentId = (int) $result;
$enrollmentId = (int) $result['id'];
if (! empty($evaluation['admin_exception']['id'])) {
$transitionService->markExceptionUsed((int) $evaluation['admin_exception']['id'], $enrollmentId);
}
@@ -642,11 +655,17 @@ class ParentController extends BaseController
if ($enrollment !== null) {
// Update enrollment as withdrawn
$this->enrollmentModel->update((int) $enrollment['id'], [
$enrollmentStatusService = \Config\Services::enrollmentStatus(false);
$enrollmentStatusService->upsertStatus([
'id' => (int) $enrollment['id'],
'student_id' => (int) $studentId,
'parent_id' => (int) ($enrollment['parent_id'] ?? $parentId),
'school_year' => (string) $this->schoolYear,
'semester' => (string) ($enrollment['semester'] ?? $this->semester),
'withdrawal_date' => local_date(utc_now(), 'Y-m-d'),
'enrollment_status' => 'withdraw under review', // Withdrawal needs review
'updated_at' => utc_now()
]);
], (int) $parentId, 'parent_withdrawal_requested');
log_message('info', "Student ID $studentId has been withdrawn from enrollment ID {$enrollment['id']}.");
// === Trigger refund process ===
+18 -8
View File
@@ -609,17 +609,27 @@ class PaymentController extends ResourceController
$db = $this->db;
$db->transBegin();
try {
$builder = $db->table('enrollments');
$builder->set('enrollment_status', 'enrolled')
->set('enrollment_date', local_date(utc_now(), 'Y-m-d')) // or 'CURRENT_DATE()' with false flag
->whereIn('id', $toUpdateIds);
$rowsToUpdate = $db->table('enrollments')
->whereIn('id', $toUpdateIds)
->get()
->getResultArray();
$statusService = \Config\Services::enrollmentStatus(false);
if ($builder->update() === false) {
$err = $db->error();
throw new \RuntimeException('Enrollments update failed: ' . ($err['message'] ?? 'unknown DB error'));
foreach ($rowsToUpdate as $row) {
$statusService->upsertStatus([
'id' => (int) $row['id'],
'student_id' => (int) $row['student_id'],
'parent_id' => (int) $row['parent_id'],
'school_year' => (string) $row['school_year'],
'semester' => (string) ($row['semester'] ?? ''),
'enrollment_date' => local_date(utc_now(), 'Y-m-d'),
'enrollment_status' => 'enrolled',
'admission_status' => 'accepted',
'updated_at' => utc_now(),
], (int) (session()->get('user_id') ?? 0) ?: null, 'payment_completed');
}
$affected = $db->affectedRows();
$affected = count($rowsToUpdate);
$db->transCommit();
log_message(
+11 -247
View File
@@ -15,7 +15,6 @@ use App\Models\StudentAllergyModel;
use App\Models\StudentMedicalConditionModel;
use App\Support\Enrollment\DeliberationDecision;
use CodeIgniter\Database\Exceptions\DataException;
use Config\Services;
use Throwable;
class StudentController extends BaseController
@@ -225,14 +224,20 @@ class StudentController extends BaseController
if ($enroll) {
$enPk = $this->enrollmentModel->primaryKey ?? 'id';
if (!$this->enrollmentModel->update($enroll[$enPk], [
$result = \Config\Services::enrollmentStatus(false)->upsertStatus([
'id' => (int) $enroll[$enPk],
'student_id' => $studentId,
'parent_id' => (int) ($enroll['parent_id'] ?? 0),
'school_year' => (string) $this->schoolYear,
'semester' => (string) $this->semester,
'class_section_id' => $primarySectionId,
'enrollment_status' => 'payment pending',
// Ensure admission is marked accepted once moved out of review
'admission_status' => 'accepted',
'updated_at' => $now,
])) {
throw new \RuntimeException('Failed to update enrollment: ' . json_encode($this->enrollmentModel->errors()));
], $userId ?: null, 'student_class_assignment');
if ((int) ($result['id'] ?? 0) <= 0) {
throw new \RuntimeException('Failed to update enrollment.');
}
}
@@ -387,242 +392,6 @@ class StudentController extends BaseController
}
}
public function removedStudents()
{
$schoolYear = (string)($this->schoolYear ?? '');
$classMap = [];
$activeYearStudentIds = [];
$classQuery = $this->db->table('student_class sc')
->select('sc.student_id, cs.class_section_name')
->join('classSection cs', 'cs.class_section_id = sc.class_section_id', 'left');
if ($schoolYear !== '') {
$classQuery->where('sc.school_year', $schoolYear);
}
$classRows = $classQuery->get()->getResultArray();
foreach ($classRows as $row) {
$sid = (int)($row['student_id'] ?? 0);
if ($sid > 0) {
$activeYearStudentIds[$sid] = true;
}
$name = trim((string)($row['class_section_name'] ?? ''));
if ($sid <= 0 || $name === '') continue;
$classMap[$sid][] = $name;
}
$students = $this->studentModel
->select('id, school_id, firstname, lastname, gender, age, is_active')
->orderBy('lastname', 'ASC')
->orderBy('firstname', 'ASC')
->findAll();
$activeStudents = [];
$removedStudents = [];
foreach ($students as $student) {
$studentId = (int)($student['id'] ?? 0);
$isGloballyActive = (int)($student['is_active'] ?? 0) === 1;
$hasSelectedYearClass = $studentId > 0 && isset($activeYearStudentIds[$studentId]);
if ($isGloballyActive && $hasSelectedYearClass) {
$activeStudents[] = $student;
} else {
$removedStudents[] = $student;
}
}
$attachClassNames = static function (array $students) use ($classMap): array {
foreach ($students as &$student) {
$sid = (int)($student['id'] ?? 0);
$names = $classMap[$sid] ?? [];
$names = array_values(array_unique(array_filter($names)));
$student['class_sections'] = $names;
$student['class_section_name'] = !empty($names) ? implode(', ', $names) : 'No class assigned';
}
unset($student);
return $students;
};
return view('administrator/removed_students', [
'active_students' => $attachClassNames($activeStudents),
'removed_students' => $attachClassNames($removedStudents),
'school_year' => $schoolYear,
'active_school_year' => (string)($this->schoolYear ?? ''),
]);
}
public function setStudentActive()
{
$studentId = (int) $this->request->getPost('student_id');
$isActiveRaw = (string) $this->request->getPost('is_active');
$isActive = $isActiveRaw === '1' ? 1 : 0;
$now = utc_now();
$userId = (int) (session()->get('user_id') ?? 0);
if ($studentId <= 0) {
return redirect()->back()->with('error', 'Invalid student ID.');
}
$student = $this->studentModel->find($studentId);
if (!$student) {
return redirect()->back()->with('error', 'Student not found.');
}
if (!$this->studentModel->update($studentId, ['is_active' => $isActive])) {
return redirect()->back()->with('error', 'Unable to update student status.');
}
$message = $isActive ? 'Student restored successfully.' : 'Student removed successfully.';
if ($isActive === 1) {
$hasCurrentClass = $this->studentClassModel
->where('student_id', $studentId)
->where('school_year', (string)$this->schoolYear)
->where('semester', (string)$this->semester)
->first();
if (!$hasCurrentClass) {
$lastClass = $this->studentClassModel
->where('student_id', $studentId)
->orderBy('updated_at', 'DESC')
->orderBy('created_at', 'DESC')
->orderBy('id', 'DESC')
->first();
$restoreClassId = (int)($lastClass['class_section_id'] ?? 0);
if ($restoreClassId > 0) {
$inserted = $this->studentClassModel->insert([
'student_id' => $studentId,
'class_section_id' => $restoreClassId,
'semester' => (string)$this->semester,
'school_year' => (string)$this->schoolYear,
'description' => $lastClass['description'] ?? null,
'updated_by' => $userId ?: null,
'updated_at' => $now,
'created_at' => $now,
]);
if ($inserted) {
$classLabel = (string)($this->classSectionModel->getClassSectionNameBySectionId($restoreClassId) ?? '');
if ($classLabel !== '') {
$message .= ' Class assignment restored to ' . $classLabel . '.';
}
} else {
$message .= ' No class assignment found for the current term.';
return redirect()->to(base_url('administrator/removed_students'))->with('warning', $message);
}
} else {
$message .= ' No class assignment found for the current term.';
return redirect()->to(base_url('administrator/removed_students'))->with('warning', $message);
}
}
}
if ($isActive === 0) {
if (!$this->notifyParentOfStudentRemoval($studentId)) {
$message .= ' Parent notification email could not be sent.';
}
}
return redirect()->to(base_url('administrator/removed_students'))->with('success', $message);
}
private function notifyParentOfStudentRemoval(int $studentId): bool
{
try {
$studentRow = $this->studentModel
->select([
'students.firstname AS student_firstname',
'students.lastname AS student_lastname',
'students.school_id',
'users.id AS parent_id',
'users.firstname AS parent_firstname',
'users.lastname AS parent_lastname',
'users.email AS parent_email',
])
->join('users', 'users.id = students.parent_id', 'left')
->where('students.id', $studentId)
->first();
if (empty($studentRow)) {
log_message('warning', "Student removal email skipped: student {$studentId} not found.");
return false;
}
$parentEmail = trim((string)($studentRow['parent_email'] ?? ''));
if ($parentEmail === '') {
log_message('warning', "Student removal email skipped: missing parent email for student {$studentId}.");
return false;
}
$studentName = trim(trim((string)($studentRow['student_firstname'] ?? '')) . ' ' . trim((string)($studentRow['student_lastname'] ?? '')));
$parentName = trim(trim((string)($studentRow['parent_firstname'] ?? '')) . ' ' . trim((string)($studentRow['parent_lastname'] ?? '')));
if ($parentName === '') {
$parentName = 'Parent/Guardian';
}
$subject = 'Withdrawal Notice for ' . ($studentName !== '' ? $studentName : 'Your Student');
$html = view('emails/student_removed', [
'student_name' => $studentName,
'school_id' => $studentRow['school_id'] ?? '',
'parent_name' => $parentName,
'signature' => 'AlRahma School Administration',
], ['saveData' => true]);
$sent = $this->sendHtmlEmail($parentEmail, $subject, $html);
$logLevel = $sent ? 'info' : 'warning';
log_message($logLevel, "Student removal email " . ($sent ? '' : 'not ') . "sent (studentId: {$studentId}, parent: {$parentEmail}).");
return $sent;
} catch (\Throwable $e) {
log_message('error', 'Student removal notification failed: ' . $e->getMessage());
return false;
}
}
private function sendHtmlEmail(string $to, string $subject, string $html): bool
{
try {
$service = function_exists('service') ? service('emailService') : null;
if (!$service && method_exists(Services::class, 'emailService')) {
$service = Services::emailService();
}
if ($service && method_exists($service, 'send')) {
$result = $service->send($to, $subject, $html, 'student-removal');
if ($result) {
return true;
}
log_message('debug', 'Custom emailService failed to send student removal notice.');
}
$email = Services::email();
$cfg = config('Email');
$fromEmail = $cfg->fromEmail ?? $cfg->SMTPUser ?? 'no-reply@example.com';
$fromName = $cfg->fromName ?? 'Al Rahma Sunday School';
$email->setTo($to);
$email->setFrom($fromEmail, $fromName);
$email->setSubject($subject);
$email->setMessage($html);
$email->setMailType('html');
$ok = $email->send();
if (!$ok) {
$debug = method_exists($email, 'printDebugger') ? $email->printDebugger(['headers', 'subject']) : 'no debugger';
log_message('debug', 'CI Email send failed for student removal: ' . print_r($debug, true));
}
return $ok;
} catch (\Throwable $e) {
log_message('debug', 'sendHtmlEmail exception for student removal: ' . $e->getMessage());
return false;
}
}
private function updateStudentAttendanceSection(
int $studentId,
int $newClassSectionId,
@@ -2742,7 +2511,6 @@ class StudentController extends BaseController
'rfid_tag' => 'permit_empty|max_length[100]',
'semester' => 'permit_empty|in_list[Fall,Spring,Summer]',
'is_new' => 'required|in_list[0,1]',
'is_active' => 'permit_empty|in_list[0,1]',
// Free-text lists; parsed later
'medical_conditions' => 'permit_empty',
@@ -2777,7 +2545,6 @@ class StudentController extends BaseController
'rfid_tag' => trim((string) $request->getPost('rfid_tag')),
'semester' => trim((string) $request->getPost('semester')),
'is_new' => (string) $request->getPost('is_new', FILTER_SANITIZE_NUMBER_INT),
'is_active' => (string) ($rawPost['is_active'] ?? ''),
// raw lists (may be missing from POST entirely)
'medical_conditions' => (string) ($rawPost['medical_conditions'] ?? ''),
@@ -2841,11 +2608,8 @@ class StudentController extends BaseController
'school_year' => $in['school_year'] ?: null,
'rfid_tag' => $in['rfid_tag'] ?: null,
'semester' => $in['semester'] ?: null,
'is_new' => (int) ($in['is_new'] === '1'),
];
if (array_key_exists('is_active', $rawPost)) {
$studentData['is_active'] = (int) ($in['is_active'] === '1');
}
'is_new' => (int) ($in['is_new'] === '1'),
];
// Normalize health lists (server-side safety)
$normConditions = $this->normalizeHealthList($in['medical_conditions'], 100); // -> condition_name
+1 -1
View File
@@ -177,4 +177,4 @@ public function getEnrollmentStatus(int $studentId, string $schoolYear): ?string
return $row['enrollment_status'] ?? null;
}
}
}
+3
View File
@@ -176,6 +176,7 @@ class StudentModel extends Model
->join('student_class', 'student_class.student_id = students.id', 'left')
->join('classSection', 'student_class.class_section_id = classSection.class_section_id', 'left')
->where('student_class.school_year', $schoolYear)
->where('students.is_active', 1)
->get()
->getResultArray();
}
@@ -200,6 +201,7 @@ class StudentModel extends Model
->join('teacher_class', 'student_class.class_section_id = teacher_class.class_section_id', 'left')
->join('classSection', 'student_class.class_section_id = classSection.class_section_id', 'left')
->where('teacher_class.teacher_id', $teacherId)
->where('students.is_active', 1)
->get()
->getResultArray();
}
@@ -215,6 +217,7 @@ class StudentModel extends Model
return $this->db->table('students')
->join('student_class', 'students.id = student_class.student_id', 'left')
->where('student_class.class_section_id', $classSectionId)
->where('students.is_active', 1)
->select('students.*, student_class.class_section_id')
->get()
->getResultArray();
+215
View File
@@ -0,0 +1,215 @@
<?php
namespace App\Services;
use CodeIgniter\Database\BaseConnection;
use InvalidArgumentException;
use RuntimeException;
final class EnrollmentStatusService
{
public const ACTIVE_STATUSES = [
'admission under review',
'review & decision',
'payment pending',
'enrolled',
'withdraw under review',
'refund pending',
];
public const INACTIVE_STATUSES = [
'denied',
'withdrawn',
'waitlist',
];
public const VALID_STATUSES = [
'admission under review',
'review & decision',
'payment pending',
'enrolled',
'withdraw under review',
'refund pending',
'withdrawn',
'waitlist',
'denied',
];
public function __construct(private readonly BaseConnection $db)
{
}
public function isActiveStatus(string $status): bool
{
return in_array($this->normalizeStatus($status), self::ACTIVE_STATUSES, true);
}
public function activeFlagForStatus(string $status): int
{
return $this->isActiveStatus($status) ? 1 : 0;
}
public function normalizeStatus(string $status): string
{
$status = strtolower(trim(str_replace("\xc2\xa0", ' ', $status)));
$status = preg_replace('/\s+/', ' ', $status) ?? $status;
if (! in_array($status, self::VALID_STATUSES, true)) {
throw new InvalidArgumentException("Invalid enrollment status: {$status}");
}
return $status;
}
public function upsertStatus(array $payload, ?int $performedBy = null, string $reason = 'enrollment_status_changed'): array
{
$studentId = (int) ($payload['student_id'] ?? 0);
$schoolYear = trim((string) ($payload['school_year'] ?? ''));
$status = $this->normalizeStatus((string) ($payload['enrollment_status'] ?? ''));
if ($studentId <= 0 || $schoolYear === '') {
throw new InvalidArgumentException('student_id and school_year are required.');
}
$now = function_exists('utc_now') ? utc_now() : date('Y-m-d H:i:s');
$payload['student_id'] = $studentId;
$payload['school_year'] = $schoolYear;
$payload['enrollment_status'] = $status;
$payload['updated_at'] = $payload['updated_at'] ?? $now;
if (! isset($payload['admission_status'])) {
$payload['admission_status'] = $this->admissionStatusFor($status);
}
if (! isset($payload['is_withdrawn']) && in_array($status, ['withdraw under review', 'refund pending', 'withdrawn'], true)) {
$payload['is_withdrawn'] = 1;
} elseif (! isset($payload['is_withdrawn']) && in_array($status, ['admission under review', 'review & decision', 'payment pending', 'enrolled', 'waitlist', 'denied'], true)) {
$payload['is_withdrawn'] = 0;
}
$this->db->transStart();
$existing = $this->controllingEnrollment($studentId, $schoolYear);
$oldStatus = $existing['enrollment_status'] ?? null;
if ($existing !== null) {
$this->db->table('enrollments')
->where('id', (int) $existing['id'])
->update($this->filterPayload('enrollments', $payload));
$enrollmentId = (int) $existing['id'];
} else {
$payload['created_at'] = $payload['created_at'] ?? $now;
$insertPayload = $this->filterPayload('enrollments', $payload);
$this->db->table('enrollments')->insert($insertPayload);
$enrollmentId = (int) $this->db->insertID();
}
$currentSchoolYear = $this->currentSchoolYear();
$updatedStudent = false;
if ($currentSchoolYear === '' || $schoolYear === $currentSchoolYear) {
$updatedStudent = $this->syncStudentActivityForEnrollment($studentId, $status);
}
$newSnapshot = $payload;
$newSnapshot['id'] = $enrollmentId;
$newSnapshot['old_enrollment_status'] = $oldStatus;
$newSnapshot['new_enrollment_status'] = $status;
$newSnapshot['student_is_active'] = $this->activeFlagForStatus($status);
$newSnapshot['student_activity_synced'] = $updatedStudent;
$this->audit($studentId, $schoolYear, $performedBy, $existing, $newSnapshot, $reason);
$this->db->transComplete();
if ($this->db->transStatus() === false) {
throw new RuntimeException('Unable to update enrollment status.');
}
return [
'id' => $enrollmentId,
'old_status' => $oldStatus,
'new_status' => $status,
'student_is_active' => $this->activeFlagForStatus($status),
'student_activity_synced' => $updatedStudent,
];
}
public function syncStudentActivityForEnrollment(int $studentId, string $status): bool
{
if ($studentId <= 0 || ! $this->db->fieldExists('is_active', 'students')) {
return false;
}
return (bool) $this->db->table('students')
->where('id', $studentId)
->update(['is_active' => $this->activeFlagForStatus($status)]);
}
public function controllingEnrollment(int $studentId, string $schoolYear, ?string $semester = null): ?array
{
$builder = $this->db->table('enrollments')
->where('student_id', $studentId)
->where('school_year', $schoolYear);
if ($semester !== null && $semester !== '') {
$builder->where('semester', $semester);
}
return $builder
->orderBy('updated_at', 'DESC')
->orderBy('enrollment_date', 'DESC')
->orderBy('id', 'DESC')
->limit(1)
->get()
->getRowArray() ?: null;
}
public function currentSchoolYear(): string
{
if (! $this->db->tableExists('configuration')) {
return '';
}
$row = $this->db->table('configuration')
->select('config_value')
->where('config_key', 'school_year')
->limit(1)
->get()
->getRowArray();
return trim((string) ($row['config_value'] ?? ''));
}
private function admissionStatusFor(string $status): string
{
if ($status === 'denied') {
return 'denied';
}
return in_array($status, ['payment pending', 'enrolled'], true) ? 'accepted' : 'pending';
}
private function filterPayload(string $table, array $payload): array
{
$fields = $this->db->getFieldNames($table);
return array_intersect_key($payload, array_flip($fields));
}
private function audit(int $studentId, string $schoolYear, ?int $performedBy, ?array $original, array $new, string $reason): void
{
if (! $this->db->tableExists('enrollment_transition_audits')) {
return;
}
$this->db->table('enrollment_transition_audits')->insert([
'student_id' => $studentId,
'school_year' => $schoolYear,
'source_school_year' => $original['source_school_year'] ?? $new['source_school_year'] ?? null,
'action' => 'enrollment_status_sync',
'performed_by' => $performedBy,
'original_values_json' => $original !== null ? json_encode($original, JSON_UNESCAPED_SLASHES) : null,
'new_values_json' => json_encode($new, JSON_UNESCAPED_SLASHES),
'reason' => $reason,
'created_at' => function_exists('utc_now') ? utc_now() : date('Y-m-d H:i:s'),
]);
}
}
+7 -2
View File
@@ -285,12 +285,17 @@ final class EnrollmentTransitionService
];
if ($original !== null) {
$this->db->table('enrollments')->where('id', (int) $original['id'])->update($payload);
$payload['id'] = (int) $original['id'];
} else {
$payload['created_at'] = date('Y-m-d H:i:s');
$this->db->table('enrollments')->insert($payload);
}
(new EnrollmentStatusService($this->db))->upsertStatus(
$payload,
$performedBy,
'initial_transition_applied'
);
if ((int) ($evaluation['assigned_class_section_id'] ?? 0) > 0) {
$this->upsertStudentClass($studentId, (int) $evaluation['assigned_class_section_id'], $targetSchoolYear, $performedBy);
}
@@ -54,7 +54,7 @@ if (!function_exists('enrollment_admin_flag_label')) {
{
return match ($type) {
'CLASS_REASSIGNMENT_REQUIRED' => 'Assign a class',
'PENDING_MAKE_UP_EXAM_PROMOTION' => 'Makeup exam result',
'PENDING_MAKE_UP_EXAM_PROMOTION' => 'Makeup exam',
'AGE_EXCEPTION_REQUIRED' => 'Age exception',
'LATE_REGISTRATION_EXCEPTION' => 'Late registration',
'FINANCIAL_REVIEW_REQUIRED' => 'Finance review',
@@ -359,7 +359,7 @@ $reasonCodes = is_array($exceptionReasonCodes ?? null) ? $exceptionReasonCodes :
<?php elseif ($flagTypeValue === 'PENDING_MAKE_UP_EXAM_PROMOTION'): ?>
<form method="post" action="<?= site_url('administrator/enrollment-admin/flags/' . $flagId . '/makeup-promotion') ?>" class="mb-2">
<?= csrf_field() ?>
<div class="small fw-semibold mb-1">Record the makeup exam result</div>
<div class="small fw-semibold mb-1">Record the makeup exam</div>
<select class="form-select form-select-sm mb-2" name="exam_result" required>
<option value="">Exam result</option>
<option value="passed">Passed</option>
@@ -1,135 +0,0 @@
<?php
$activeStudents = $active_students ?? [];
$removedStudents = $removed_students ?? [];
$schoolYear = $school_year ?? '';
$activeSchoolYear = $active_school_year ?? '';
?>
<?= $this->extend('layout/management_layout') ?>
<?= $this->section('content') ?>
<div class="container-fluid">
<div class="wrapper">
<div class="content">
<h2 class="text-center mt-4 mb-2">Student Removal</h2>
<p class="text-center text-muted mb-4">
Active school year: <?= esc($schoolYear !== '' ? $schoolYear : $activeSchoolYear) ?>.
Active students have a class assignment in the active school year. Removed students are inactive or have no active-year assignment.
</p>
<?= $this->include('partials/flash_messages') ?>
<div class="card mb-4 shadow-sm">
<div class="card-header d-flex align-items-center justify-content-between">
<span>Active Students</span>
<span class="badge bg-primary"><?= count($activeStudents) ?></span>
</div>
<div class="card-body">
<div class="table-responsive">
<table id="activeStudentsTable" class="table table-striped table-hover align-middle">
<thead class="table-dark">
<tr>
<th>School ID</th>
<th>First Name</th>
<th>Last Name</th>
<th>Class</th>
<th style="min-width: 140px;">Action</th>
</tr>
</thead>
<tbody>
<?php if (!empty($activeStudents)): ?>
<?php foreach ($activeStudents as $student): ?>
<tr>
<td><?= esc($student['school_id'] ?? '') ?></td>
<td><?= esc($student['firstname'] ?? '') ?></td>
<td><?= esc($student['lastname'] ?? '') ?></td>
<td><?= esc($student['class_section_name'] ?? 'No class assigned') ?></td>
<td>
<form method="post" action="<?= site_url('administrator/students/set-active') ?>" class="d-inline">
<?= csrf_field() ?>
<input type="hidden" name="student_id" value="<?= (int)($student['id'] ?? 0) ?>">
<input type="hidden" name="is_active" value="0">
<button type="submit" class="btn btn-outline-danger btn-sm" onclick="return confirm('Remove this student from classes and attendance?');">
Remove
</button>
</form>
</td>
</tr>
<?php endforeach; ?>
<?php else: ?>
<tr>
<td colspan="5" class="text-center">No active students found.</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
</div>
<div class="card shadow-sm">
<div class="card-header d-flex align-items-center justify-content-between">
<span>Removed Students</span>
<span class="badge bg-secondary"><?= count($removedStudents) ?></span>
</div>
<div class="card-body">
<div class="table-responsive">
<table id="removedStudentsTable" class="table table-striped table-hover align-middle">
<thead class="table-dark">
<tr>
<th>School ID</th>
<th>First Name</th>
<th>Last Name</th>
<th>Last Class</th>
<th style="min-width: 140px;">Action</th>
</tr>
</thead>
<tbody>
<?php if (!empty($removedStudents)): ?>
<?php foreach ($removedStudents as $student): ?>
<tr>
<td><?= esc($student['school_id'] ?? '') ?></td>
<td><?= esc($student['firstname'] ?? '') ?></td>
<td><?= esc($student['lastname'] ?? '') ?></td>
<td><?= esc($student['class_section_name'] ?? 'No class assigned') ?></td>
<td>
<form method="post" action="<?= site_url('administrator/students/set-active') ?>" class="d-inline">
<?= csrf_field() ?>
<input type="hidden" name="student_id" value="<?= (int)($student['id'] ?? 0) ?>">
<input type="hidden" name="is_active" value="1">
<button type="submit" class="btn btn-outline-success btn-sm" onclick="return confirm('Restore this student to classes and attendance?');">
Restore
</button>
</form>
</td>
</tr>
<?php endforeach; ?>
<?php else: ?>
<tr>
<td colspan="5" class="text-center">No removed students found.</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
<?= $this->endSection() ?>
<?= $this->section('scripts') ?>
<script>
document.addEventListener('DOMContentLoaded', () => {
if (window.jQuery && typeof jQuery.fn.DataTable === 'function') {
if (document.getElementById('activeStudentsTable')) {
jQuery('#activeStudentsTable').DataTable();
}
if (document.getElementById('removedStudentsTable')) {
jQuery('#removedStudentsTable').DataTable();
}
}
});
</script>
<?= $this->endSection() ?>
+4 -6
View File
@@ -381,14 +381,12 @@ $selectedYear = trim((string)($selectedYear ?? ''));
</select>
</div>
<!-- is_active -->
<div class="col-md-3">
<label class="form-label d-block">Active Status</label>
<input type="hidden" name="is_active" value="0">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" value="1" id="is_active_<?= (int)$student['id'] ?>" name="is_active" <?= $isActive ? 'checked' : '' ?>>
<label class="form-check-label" for="is_active_<?= (int)$student['id'] ?>">Show in classes/attendance</label>
</div>
<span class="badge <?= $isActive ? 'bg-success' : 'bg-secondary' ?>">
<?= $isActive ? 'Visible' : 'Hidden' ?>
</span>
<div class="form-text">Controlled by current enrollment status.</div>
</div>
<!-- photo_consent -->
-26
View File
@@ -1,26 +0,0 @@
<?= $this->extend('layout/email_layout') ?>
<?= $this->section('content') ?>
<div style="font-family:Arial, Helvetica, sans-serif; color:#222; font-size:16px; line-height:1.6;">
<p style="margin:0 0 12px 0;">
Dear <?= esc($parent_name ?? 'Parent/Guardian') ?>,
</p>
<p style="margin:0 0 12px 0;">
We are writing to inform you that <?= $student_name ? 'your child, <strong>' . esc($student_name) . '</strong>' : 'your child' ?> has been officially withdrawn from the school's enrollment list, effective immediately.
</p>
<p style="margin:0 0 12px 0;">
To ensure confidentiality and accuracy, we kindly request that you contact the school directly. This will allow us to provide complete information and discuss any necessary next steps.
</p>
<p style="margin:0 0 12px 0;">
Thank you for your understanding. We look forward to speaking with you soon.
</p>
<p style="margin:0;">
Sincerely,<br>
<?= esc($signature ?? 'AlRahma School Administration') ?>
</p>
</div>
<?= $this->endSection() ?>
+7 -3
View File
@@ -525,7 +525,7 @@
<!-- Statistics Start -->
<div class="container-xxl py-2 content-section">
<div class="container">
<div class="home-stats" data-dashboard-endpoint="<?= site_url('api/administrator/dashboard') ?>">
<div class="home-stats" data-dashboard-endpoint="/api/administrator/dashboard">
<h1 class="d-flex justify-content-center p-md-1">Active Participants</h1>
<br>
<div class="stats-row">
@@ -734,11 +734,15 @@
window.addEventListener('load', function() {
// Hide the spinner
const spinner = document.getElementById('spinner');
spinner.style.opacity = '0';
if (spinner) {
spinner.style.opacity = '0';
}
// Remove spinner from DOM after fade out
setTimeout(function() {
spinner.style.display = 'none';
if (spinner) {
spinner.style.display = 'none';
}
// Force show all content
document.querySelectorAll('.content-section, h1, h2, h3, h4, h5, h6, p').forEach(function(el) {
+2 -7
View File
@@ -454,22 +454,17 @@ foreach (($students ?? []) as $student) {
</div>
<div class="d-none" data-step-panel="2">
<h6 class="fw-semibold">Review tuition, fees, and balance</h6>
<h6 class="fw-semibold">Review tuition, fees and balance</h6>
<div class="text-muted small mb-3">Review the family account information before submitting enrollment.</div>
<?php if ($familyFinancialSummary !== []): ?>
<div class="border rounded p-3 bg-light">
<div class="row g-2">
<div class="col-md-4"><span class="text-muted">Previous-year carry-over balance:</span> <strong data-financial-value="carry_over_balance"><?= esc($money($familyFinancialSummary['carry_over_balance'] ?? 0)) ?></strong></div>
<div class="col-md-4"><span class="text-muted">Registration fee:</span> <strong data-financial-value="registration_fee"><?= esc($money($familyFinancialSummary['registration_fee'] ?? 0)) ?></strong></div>
<div class="col-md-4"><span class="text-muted">Tuition due now:</span> <strong data-financial-value="tuition_due_at_registration"><?= esc($money($familyFinancialSummary['tuition_due_at_registration'] ?? 0)) ?></strong></div>
<div class="col-md-4"><span class="text-muted">Mandatory fees:</span> <strong data-financial-value="mandatory_fees"><?= esc($money($familyFinancialSummary['mandatory_fees'] ?? 0)) ?></strong></div>
<div class="col-md-4"><span class="text-muted">Current-year account balance:</span> <strong data-financial-value="current_balance"><?= esc($money($familyFinancialSummary['current_balance'] ?? 0)) ?></strong></div>
<div class="col-md-4"><span class="text-muted">Total currently due:</span> <strong data-financial-value="amount_due"><?= esc($money($familyFinancialSummary['amount_due'] ?? 0)) ?></strong></div>
<div class="col-md-4"><span class="text-muted">Total currently due:</span> <strong data-financial-value="amount_due"><?= esc($money($familyFinancialSummary['amount_due'] ?? 0)) ?></strong></div>
</div>
<?php if (!empty($familyFinancialSummary['policy_message'])): ?>
<div class="small text-muted mt-2"><?= esc($familyFinancialSummary['policy_message']) ?></div>
<?php endif; ?>
<div class="small mt-2"><a href="<?= site_url('parent/financial-aid') ?>">Request financial aid</a></div>
</div>
<?php else: ?>
<div class="alert alert-warning mb-0">Family account information is not available. Please contact the administration if you have questions about tuition or balance.</div>
+4 -4
View File
@@ -34,19 +34,19 @@ switch ($userRole) {
<ul class="list-unstyled mb-0 info-list">
<li class="info-title text-center text-md-end"></li>
<li>
<a href="<?= base_url('privacy_policy.pdf') ?>" target="_blank" rel="noopener noreferrer"
<a href="/privacy_policy.pdf" target="_blank" rel="noopener noreferrer"
class="pdf-link" data-filename="privacy_policy.pdf">
<i class="fas fa-file-pdf me-1"></i>Privacy Policy
</a>
</li>
<li>
<a href="<?= base_url('terms_of_service.pdf') ?>" target="_blank" rel="noopener noreferrer"
<a href="/terms_of_service.pdf" target="_blank" rel="noopener noreferrer"
class="pdf-link" data-filename="terms_of_service.pdf">
<i class="fas fa-file-pdf me-1"></i>Terms of Service
</a>
</li>
<li>
<a href="<?= base_url('account_creation_guide.pdf') ?>" target="_blank" rel="noopener noreferrer"
<a href="/account_creation_guide.pdf" target="_blank" rel="noopener noreferrer"
class="pdf-link" data-filename="account_creation_guide.pdf">
<i class="fas fa-file-pdf"></i>How To Create An Account
</a>
@@ -91,4 +91,4 @@ switch ($userRole) {
});
});
});
</script>
</script>
@@ -0,0 +1,45 @@
<?php
namespace Tests\App\Services;
use App\Services\EnrollmentStatusService;
use CodeIgniter\Database\BaseConnection;
use InvalidArgumentException;
use PHPUnit\Framework\TestCase;
final class EnrollmentStatusServiceTest extends TestCase
{
/**
* @dataProvider statusMappingProvider
*/
public function testEnrollmentStatusMapsToStudentActivity(string $status, int $expected): void
{
$service = new EnrollmentStatusService($this->createMock(BaseConnection::class));
$this->assertSame($expected, $service->activeFlagForStatus($status));
}
public static function statusMappingProvider(): array
{
return [
['admission under review', 1],
['review & decision', 1],
['payment pending', 1],
['enrolled', 1],
['withdraw under review', 1],
['refund pending', 1],
['denied', 0],
['withdrawn', 0],
['waitlist', 0],
];
}
public function testUnknownStatusIsRejected(): void
{
$service = new EnrollmentStatusService($this->createMock(BaseConnection::class));
$this->expectException(InvalidArgumentException::class);
$service->activeFlagForStatus('accepted');
}
}