fix enrollment logic, add financial aid, fix class distribution
Tests / PHPUnit (push) Failing after 1m6s

This commit is contained in:
root
2026-08-15 15:07:16 -04:00
parent c12bb59372
commit 4603d9ced2
95 changed files with 6892 additions and 1295 deletions
@@ -26,8 +26,14 @@ class EnrollmentAdminController extends BaseController
$status = trim((string) ($this->request->getGet('status') ?? 'open'));
$flagType = trim((string) ($this->request->getGet('flag_type') ?? ''));
$assignedTo = trim((string) ($this->request->getGet('assigned_to') ?? ''));
$exceptionParentId = (int) ($this->request->getGet('exception_parent_id') ?? 0);
$canManageExceptions = $this->canManageEnrollmentExceptions();
service('enrollmentTransition')->syncDashboardBlockageFlags($schoolYear);
$flags = $this->enrollmentFlags($schoolYear, $status, $flagType, $assignedTo);
$openFlags = ($status === 'open' && $flagType === '' && $assignedTo === '')
? $flags
: $this->enrollmentFlags($schoolYear, 'open', '', '');
return view('administrator/enrollment_admin_dashboard', [
'flags' => $flags,
@@ -39,11 +45,19 @@ class EnrollmentAdminController extends BaseController
'schoolYears' => $this->schoolYears(),
'classSections' => $this->classSections($schoolYear),
'admins' => $this->adminUsers(),
'enrollmentFollowups' => $this->enrollmentFollowups($schoolYear),
'enrollmentFollowups' => $this->enrollmentFollowups($schoolYear, $openFlags),
'auditRows' => $this->auditRows($schoolYear),
'activeExceptions' => $canManageExceptions ? $this->enrollmentExceptions($schoolYear) : [],
'exceptionNeeded' => $this->exceptionNeededFromFlags($openFlags),
'canManageEnrollmentExceptions' => $canManageExceptions,
'exceptionFamilies' => $this->exceptionFamilies(),
'exceptionReasonCodes' => $this->exceptionReasonCodes(),
'exceptionSelectedParentId' => $exceptionParentId,
'exceptionPreview' => $this->exceptionPreview($exceptionParentId, $schoolYear),
'launchState' => $this->launchState($schoolYear),
'previewParentId' => $this->firstParentWithStudents(),
'emailExamples' => service('enrollmentRegistrationEmail')->previewExamplesForSchoolYear($schoolYear),
'openFlagCount' => $this->openFlagCount($schoolYear),
]);
}
@@ -202,28 +216,257 @@ class EnrollmentAdminController extends BaseController
public function approveException(int $id)
{
try {
if (! $this->canManageEnrollmentExceptions()) {
return redirect()->back()->with('error', 'You do not have permission to manage enrollment exceptions.');
}
$flag = $this->requireFlag($id);
$reason = trim((string) ($this->request->getPost('reason') ?? ''));
if ($reason === '') {
return redirect()->back()->with('error', 'Approval reason is required.');
}
$parentId = $this->parentIdForStudent((int) $flag['student_id'], (string) $flag['school_year']);
if ($parentId <= 0) {
return redirect()->back()->with('error', 'Unable to create exception because no linked parent was found.');
}
$ruleCodes = $this->ruleCodesForFlag((string) $flag['flag_type']);
$now = date('Y-m-d H:i:s');
$expiresAt = date('Y-m-d H:i:s', strtotime('+30 days'));
$this->db->transStart();
if ($this->db->tableExists('enrollment_exceptions')) {
$existing = $this->db->table('enrollment_exceptions')
->where('parent_id', $parentId)
->where('student_id', (int) $flag['student_id'])
->where('school_year', (string) $flag['school_year'])
->where('status', 'active')
->limit(1)
->get()
->getRowArray();
$payload = [
'parent_id' => $parentId,
'student_id' => (int) $flag['student_id'],
'school_year' => (string) $flag['school_year'],
'source_school_year' => $flag['source_school_year'] ?? null,
'status' => 'active',
'reason_code' => (string) $flag['flag_type'],
'reason_note' => $reason,
'bypassed_rule_codes_json' => json_encode($ruleCodes, JSON_UNESCAPED_SLASHES),
'family_student_ids_json' => json_encode([(int) $flag['student_id']], JSON_UNESCAPED_SLASHES),
'created_by' => $this->userId(),
'approved_by' => $this->userId(),
'starts_at' => $now,
'expires_at' => $expiresAt,
'updated_at' => $now,
];
if ($existing !== null) {
$this->db->table('enrollment_exceptions')->where('id', (int) $existing['id'])->update($payload);
} else {
$payload['created_at'] = $now;
$this->db->table('enrollment_exceptions')->insert($payload);
}
}
$this->db->table('enrollments')
->where('student_id', (int) $flag['student_id'])
->where('school_year', (string) $flag['school_year'])
->update([
'exception_required' => 0,
'exception_reason' => $reason,
'updated_at' => date('Y-m-d H:i:s'),
'updated_at' => $now,
]);
$this->resolveFlagRow($flag, $reason, 'enrollment_exception_approved');
$this->db->transComplete();
if ($this->db->transStatus() === false) {
return redirect()->back()->with('error', 'Unable to approve enrollment exception.');
}
return redirect()->back()->with('success', 'Enrollment exception approved.');
} catch (Throwable $e) {
return redirect()->back()->with('error', $e->getMessage());
}
}
public function createException()
{
try {
if (! $this->canManageEnrollmentExceptions()) {
return redirect()->back()->with('error', 'You do not have permission to manage enrollment exceptions.');
}
if (! $this->db->tableExists('enrollment_exceptions')) {
return redirect()->back()->with('error', 'Enrollment exception storage is not available. Run migrations first.');
}
$parentId = (int) ($this->request->getPost('parent_id') ?? 0);
$studentIds = $this->request->getPost('student_ids') ?? [];
if (! is_array($studentIds)) {
$studentIds = [$this->request->getPost('student_id') ?? 0];
}
$studentIds = array_values(array_unique(array_filter(array_map('intval', $studentIds), static fn (int $id): bool => $id > 0)));
$schoolYear = trim((string) ($this->request->getPost('school_year') ?? ''));
$reasonCode = trim((string) ($this->request->getPost('reason_code') ?? ''));
$reasonNote = trim((string) ($this->request->getPost('reason_note') ?? ''));
$expiresAt = trim((string) ($this->request->getPost('expires_at') ?? ''));
$postedCodesByStudent = $this->request->getPost('bypassed_rule_codes_by_student') ?? [];
$postedCodesByStudent = is_array($postedCodesByStudent) ? $postedCodesByStudent : [];
if ($parentId <= 0 || $studentIds === [] || $schoolYear === '' || $reasonCode === '' || $reasonNote === '') {
return redirect()->back()->withInput()->with('error', 'Parent, at least one student, school year, reason code, and note are required.');
}
$sourceSchoolYear = $this->previousSchoolYearName($schoolYear);
if ($sourceSchoolYear === null) {
return redirect()->back()->withInput()->with('error', 'Unable to determine the source school year.');
}
if ($expiresAt === '') {
$expiresAt = date('Y-m-d H:i:s', strtotime('+30 days'));
} else {
try {
$expiresAt = (new \DateTimeImmutable($expiresAt))->format('Y-m-d H:i:s');
} catch (\Throwable) {
return redirect()->back()->withInput()->with('error', 'Expiration date is invalid.');
}
}
$now = date('Y-m-d H:i:s');
$transitionService = service('enrollmentTransition');
$saved = 0;
$this->db->transStart();
foreach ($studentIds as $studentId) {
if (! $this->studentLinkedToParent($studentId, $parentId)) {
$this->db->transRollback();
return redirect()->back()->withInput()->with('error', 'Selected student is not linked to the selected parent.');
}
$evaluation = $transitionService->evaluateForParent($parentId, $studentId, $sourceSchoolYear, $schoolYear, 'admin');
$failedCodes = array_values(array_unique(array_filter(array_map('strval', array_merge(
$evaluation['blocking_rule_codes'] ?? [],
$evaluation['review_rule_codes'] ?? []
)))));
$postedCodes = $postedCodesByStudent[(string) $studentId] ?? $postedCodesByStudent[$studentId] ?? [];
$postedCodes = is_array($postedCodes) ? array_values(array_filter(array_map('strval', $postedCodes))) : [];
$postedCodes = array_values(array_unique(array_map(static fn (string $code): string => strtoupper(trim($code)), $postedCodes)));
$failedCodes = array_values(array_unique(array_map(static fn (string $code): string => strtoupper(trim($code)), $failedCodes)));
$ruleCodes = $postedCodes !== [] ? array_values(array_intersect($postedCodes, $failedCodes)) : $failedCodes;
$ruleCodes = array_values(array_filter($ruleCodes, static fn (string $code): bool => $code !== ''));
$nonOverridable = array_values(array_intersect($ruleCodes, ['STUDENT_NOT_LINKED', 'SOURCE_YEAR_NOT_FOUND', 'TARGET_YEAR_NOT_FOUND', 'ALREADY_ENROLLED']));
if ($nonOverridable !== []) {
$this->db->transRollback();
return redirect()->back()->withInput()->with('error', 'These rule(s) cannot be bypassed: ' . implode(', ', $nonOverridable));
}
if ($ruleCodes === []) {
$this->db->transRollback();
return redirect()->back()->withInput()->with('error', 'No failed eligibility rule codes were selected for one or more selected students.');
}
$existing = $this->db->table('enrollment_exceptions')
->where('parent_id', $parentId)
->where('student_id', $studentId)
->where('school_year', $schoolYear)
->where('status', 'active')
->limit(1)
->get()
->getRowArray();
$payload = [
'parent_id' => $parentId,
'student_id' => $studentId,
'school_year' => $schoolYear,
'source_school_year' => $sourceSchoolYear,
'status' => 'active',
'reason_code' => $reasonCode,
'reason_note' => $reasonNote,
'bypassed_rule_codes_json' => json_encode($ruleCodes, JSON_UNESCAPED_SLASHES),
'family_student_ids_json' => json_encode([$studentId], JSON_UNESCAPED_SLASHES),
'created_by' => $this->userId(),
'approved_by' => $this->userId(),
'starts_at' => $now,
'expires_at' => $expiresAt,
'updated_at' => $now,
];
if ($existing !== null) {
$this->db->table('enrollment_exceptions')->where('id', (int) $existing['id'])->update($payload);
$exceptionId = (int) $existing['id'];
} else {
$payload['created_at'] = $now;
$this->db->table('enrollment_exceptions')->insert($payload);
$exceptionId = (int) $this->db->insertID();
}
$this->audit($studentId, $schoolYear, $sourceSchoolYear, 'enrollment_exception_created', $existing, array_merge($payload, [
'id' => $exceptionId,
'evaluation_decision' => $evaluation['decision'] ?? null,
]), $reasonNote);
$this->resolveCoveredFlags($studentId, $schoolYear, $ruleCodes, $reasonNote);
$saved++;
}
$this->db->transComplete();
if ($this->db->transStatus() === false) {
return redirect()->back()->withInput()->with('error', 'Unable to save enrollment exception.');
}
return redirect()->to(site_url('administrator/enrollment-admin?school_year=' . rawurlencode($schoolYear) . '&exception_parent_id=' . $parentId))
->with('success', $saved . ' enrollment exception' . ($saved === 1 ? '' : 's') . ' saved.');
} catch (Throwable $e) {
return redirect()->back()->withInput()->with('error', $e->getMessage());
}
}
public function revokeException(int $id)
{
if (! $this->canManageEnrollmentExceptions()) {
return redirect()->back()->with('error', 'You do not have permission to manage enrollment exceptions.');
}
if ($id <= 0 || ! $this->db->tableExists('enrollment_exceptions')) {
return redirect()->back()->with('error', 'Enrollment exception was not found.');
}
$reason = trim((string) ($this->request->getPost('revocation_reason') ?? ''));
if ($reason === '') {
return redirect()->back()->with('error', 'Revocation reason is required.');
}
$exception = $this->db->table('enrollment_exceptions')->where('id', $id)->limit(1)->get()->getRowArray();
if ($exception === null || (string) ($exception['status'] ?? '') !== 'active') {
return redirect()->back()->with('error', 'Only active exceptions can be revoked.');
}
$this->db->table('enrollment_exceptions')->where('id', $id)->update([
'status' => 'revoked',
'revoked_at' => date('Y-m-d H:i:s'),
'revoked_by' => $this->userId(),
'revocation_reason' => $reason,
'updated_at' => date('Y-m-d H:i:s'),
]);
$this->audit(
(int) $exception['student_id'],
(string) $exception['school_year'],
(string) ($exception['source_school_year'] ?? ''),
'enrollment_exception_revoked',
$exception,
['status' => 'revoked', 'revocation_reason' => $reason],
$reason
);
return redirect()->back()->with('success', 'Enrollment exception revoked.');
}
private function enrollmentFlags(string $schoolYear, string $status, string $flagType, string $assignedTo): array
{
if (! $this->db->tableExists('enrollment_flags')) {
@@ -232,10 +475,12 @@ class EnrollmentAdminController extends BaseController
$builder = $this->db->table('enrollment_flags ef')
->select('ef.*')
->select('s.firstname, s.lastname, s.school_id')
->select('s.firstname, s.lastname, s.school_id, s.parent_id')
->select('u.firstname AS assignee_firstname, u.lastname AS assignee_lastname')
->select('p.firstname AS parent_firstname, p.lastname AS parent_lastname')
->join('students s', 's.id = ef.student_id', 'left')
->join('users u', 'u.id = ef.assigned_to', 'left')
->join('users p', 'p.id = s.parent_id', 'left')
->orderBy('ef.created_at', 'DESC')
->orderBy('ef.id', 'DESC');
@@ -256,6 +501,7 @@ class EnrollmentAdminController extends BaseController
foreach ($rows as &$row) {
$row['student_name'] = trim((string) ($row['firstname'] ?? '') . ' ' . (string) ($row['lastname'] ?? '')) ?: 'Student #' . (int) ($row['student_id'] ?? 0);
$row['assignee_name'] = trim((string) ($row['assignee_firstname'] ?? '') . ' ' . (string) ($row['assignee_lastname'] ?? ''));
$row['parent_name'] = trim((string) ($row['parent_firstname'] ?? '') . ' ' . (string) ($row['parent_lastname'] ?? '')) ?: ((int) ($row['parent_id'] ?? 0) > 0 ? 'Parent #' . (int) $row['parent_id'] : '');
$row['details'] = json_decode((string) ($row['details_json'] ?? ''), true) ?: [];
}
unset($row);
@@ -263,10 +509,10 @@ class EnrollmentAdminController extends BaseController
return $rows;
}
private function enrollmentFollowups(string $schoolYear): array
private function enrollmentFollowups(string $schoolYear, array $flags = []): array
{
if (! $this->db->tableExists('enrollments')) {
return [];
return $this->followupsFromFlags($flags, []);
}
$fields = $this->db->getFieldNames('enrollments');
@@ -338,10 +584,16 @@ class EnrollmentAdminController extends BaseController
if (in_array('deliberation_decision', $fields, true)) {
$builder->orWhereIn('e.deliberation_decision', [
'make_up_exam',
'MAKE_UP_EXAM',
'repeat_class',
'REPEAT_CLASS',
'deferred',
'DEFERRED',
'DEFERRED_DECISION',
'expelled',
'EXPELLED',
'withdrawn',
'WITHDRAWN',
]);
}
@@ -351,9 +603,76 @@ class EnrollmentAdminController extends BaseController
}
unset($row);
return $this->followupsFromFlags($flags, $rows);
}
private function followupsFromFlags(array $flags, array $rows): array
{
$seen = [];
foreach ($rows as $row) {
$seen[(int) ($row['student_id'] ?? 0)] = true;
}
$followupTypes = [
'PENDING_MAKE_UP_EXAM_PROMOTION' => 'temporary_same_grade',
'CLASS_REASSIGNMENT_REQUIRED' => 'manual_class_required',
'CLASS_CAPACITY_EXCEPTION_REQUIRED' => 'manual_class_required',
'COMPLETION_OR_EXIT_PROCESS_REQUIRED' => 'exit_required',
];
foreach ($flags as $flag) {
$type = (string) ($flag['flag_type'] ?? '');
$studentId = (int) ($flag['student_id'] ?? 0);
if ($studentId <= 0 || isset($seen[$studentId]) || ! isset($followupTypes[$type])) {
continue;
}
$seen[$studentId] = true;
$rows[] = [
'id' => 0,
'student_id' => $studentId,
'student_name' => (string) ($flag['student_name'] ?? ''),
'school_id' => (string) ($flag['school_id'] ?? ''),
'enrollment_status' => 'not enrolled',
'deliberation_decision' => $type === 'PENDING_MAKE_UP_EXAM_PROMOTION' ? 'MAKE_UP_EXAM' : '',
'placement_status' => $followupTypes[$type],
'class_section_name' => '',
'exception_required' => 0,
'exception_reason' => $type,
'updated_at' => $flag['created_at'] ?? null,
];
}
return $rows;
}
private function exceptionNeededFromFlags(array $flags): array
{
$exceptionTypes = [
'AGE_EXCEPTION_REQUIRED',
'LATE_REGISTRATION_EXCEPTION',
'FINANCIAL_REVIEW_REQUIRED',
'CLASS_CAPACITY_EXCEPTION_REQUIRED',
'SIBLING_LAST_NAME_MISMATCH',
'DEFERRED_DELIBERATION',
'WITHDRAWAL_REVIEW_REQUIRED',
'RESTRICTED_ADMINISTRATIVE_REVIEW',
];
$needed = [];
foreach ($flags as $flag) {
if (($flag['status'] ?? 'open') !== 'open') {
continue;
}
if (! in_array((string) ($flag['flag_type'] ?? ''), $exceptionTypes, true)) {
continue;
}
$needed[] = $flag;
}
return $needed;
}
private function requireFlag(int $id): array
{
if ($id <= 0 || ! $this->db->tableExists('enrollment_flags')) {
@@ -392,6 +711,29 @@ class EnrollmentAdminController extends BaseController
}
}
private function resolveCoveredFlags(int $studentId, string $schoolYear, array $ruleCodes, string $notes): void
{
if ($studentId <= 0 || $schoolYear === '' || $ruleCodes === [] || ! $this->db->tableExists('enrollment_flags')) {
return;
}
$ruleCodes = array_values(array_unique(array_map(static fn (string $code): string => strtoupper(trim($code)), $ruleCodes)));
$flags = $this->db->table('enrollment_flags')
->where('student_id', $studentId)
->where('school_year', $schoolYear)
->where('status', 'open')
->get()
->getResultArray();
foreach ($flags as $flag) {
$flagType = strtoupper(trim((string) ($flag['flag_type'] ?? '')));
$mapped = array_map('strtoupper', $this->ruleCodesForFlag($flagType));
if (in_array($flagType, $ruleCodes, true) || array_intersect($mapped, $ruleCodes) !== []) {
$this->resolveFlagRow($flag, $notes, 'enrollment_exception_created');
}
}
}
private function applyClassSection(int $studentId, string $schoolYear, int $sectionId, string $auditAction, string $placementStatus = 'manual_class_assigned'): void
{
$section = $this->db->table('classSection')
@@ -514,6 +856,270 @@ class EnrollmentAdminController extends BaseController
return $rows;
}
private function enrollmentExceptions(string $schoolYear): array
{
if (! $this->db->tableExists('enrollment_exceptions')) {
return [];
}
$builder = $this->db->table('enrollment_exceptions ee')
->select('ee.*')
->select('s.firstname, s.lastname, s.school_id')
->select('u.firstname AS parent_firstname, u.lastname AS parent_lastname')
->select('admin.firstname AS admin_firstname, admin.lastname AS admin_lastname')
->join('students s', 's.id = ee.student_id', 'left')
->join('users u', 'u.id = ee.parent_id', 'left')
->join('users admin', 'admin.id = ee.created_by', 'left')
->orderBy('ee.created_at', 'DESC')
->orderBy('ee.id', 'DESC')
->limit(100);
if ($schoolYear !== '') {
$builder->where('ee.school_year', $schoolYear);
}
$rows = $builder->get()->getResultArray();
foreach ($rows as &$row) {
$row['student_name'] = trim((string) ($row['firstname'] ?? '') . ' ' . (string) ($row['lastname'] ?? '')) ?: 'Student #' . (int) ($row['student_id'] ?? 0);
$row['parent_name'] = trim((string) ($row['parent_firstname'] ?? '') . ' ' . (string) ($row['parent_lastname'] ?? '')) ?: 'Parent #' . (int) ($row['parent_id'] ?? 0);
$row['created_by_name'] = trim((string) ($row['admin_firstname'] ?? '') . ' ' . (string) ($row['admin_lastname'] ?? '')) ?: ((int) ($row['created_by'] ?? 0) > 0 ? 'User #' . (int) $row['created_by'] : '');
$decoded = json_decode((string) ($row['bypassed_rule_codes_json'] ?? ''), true);
$row['bypassed_rule_codes'] = is_array($decoded) ? array_values(array_map('strval', $decoded)) : [];
}
unset($row);
return $rows;
}
private function exceptionFamilies(): array
{
if (! $this->db->tableExists('students') || ! $this->db->tableExists('users')) {
return [];
}
$rows = $this->db->table('students s')
->select('s.id AS student_id, s.firstname AS student_firstname, s.lastname AS student_lastname, s.school_id, s.parent_id')
->select('u.firstname AS parent_firstname, u.lastname AS parent_lastname, u.email AS parent_email')
->join('users u', 'u.id = s.parent_id', 'left')
->where('s.parent_id >', 0)
->orderBy('u.lastname', 'ASC')
->orderBy('u.firstname', 'ASC')
->orderBy('s.lastname', 'ASC')
->orderBy('s.firstname', 'ASC')
->limit(1000)
->get()
->getResultArray();
$families = [];
foreach ($rows as $row) {
$parentId = (int) ($row['parent_id'] ?? 0);
if ($parentId <= 0) {
continue;
}
if (! isset($families[$parentId])) {
$parentName = trim((string) ($row['parent_firstname'] ?? '') . ' ' . (string) ($row['parent_lastname'] ?? ''));
$families[$parentId] = [
'parent_id' => $parentId,
'parent_name' => $parentName !== '' ? $parentName : 'Parent #' . $parentId,
'parent_email' => (string) ($row['parent_email'] ?? ''),
'students' => [],
];
}
$studentId = (int) ($row['student_id'] ?? 0);
$studentName = trim((string) ($row['student_firstname'] ?? '') . ' ' . (string) ($row['student_lastname'] ?? ''));
$families[$parentId]['students'][] = [
'student_id' => $studentId,
'student_name' => $studentName !== '' ? $studentName : 'Student #' . $studentId,
'school_id' => (string) ($row['school_id'] ?? ''),
];
}
$families = array_values($families);
usort($families, static function (array $left, array $right): int {
return strnatcasecmp(
(string) ($left['parent_name'] ?? ''),
(string) ($right['parent_name'] ?? '')
);
});
return $families;
}
private function exceptionReasonCodes(): array
{
return [
'ADMIN_REVIEW_APPROVED' => 'Administrative review approved',
'FINANCE_APPROVAL_REQUIRED' => 'Finance approval',
'SIBLING_LAST_NAME_REVIEWED' => 'Sibling last name reviewed',
'AGE_EXCEPTION_APPROVED' => 'Age exception approved',
'LATE_REGISTRATION_APPROVED' => 'Late registration approved',
'ACADEMIC_STATUS_EXCEPTION' => 'Academic status exception',
'MANUAL_PLACEMENT_EXCEPTION' => 'Manual placement exception',
];
}
private function exceptionPreview(int $parentId, string $schoolYear): ?array
{
if ($parentId <= 0 || $schoolYear === '') {
return null;
}
$sourceSchoolYear = $this->previousSchoolYearName($schoolYear);
if ($sourceSchoolYear === null) {
return [
'error' => 'Unable to determine the previous school year for ' . $schoolYear . '.',
];
}
$parent = $this->db->table('users u')
->select('u.firstname, u.lastname, u.email')
->where('u.id', $parentId)
->limit(1)
->get()
->getRowArray() ?: [];
$studentRows = $this->db->table('students s')
->select('s.id, s.firstname, s.lastname, s.school_id')
->where('s.parent_id', $parentId)
->orderBy('s.lastname', 'ASC')
->orderBy('s.firstname', 'ASC')
->get()
->getResultArray();
$students = [];
foreach ($studentRows as $student) {
$linkedStudentId = (int) ($student['id'] ?? 0);
if ($linkedStudentId <= 0) {
continue;
}
$students[] = [
'student_id' => $linkedStudentId,
'student_name' => trim((string) ($student['firstname'] ?? '') . ' ' . (string) ($student['lastname'] ?? '')) ?: 'Student #' . $linkedStudentId,
'school_id' => (string) ($student['school_id'] ?? ''),
'evaluation' => service('enrollmentTransition')->evaluateForParent($parentId, $linkedStudentId, $sourceSchoolYear, $schoolYear, 'admin'),
];
}
return [
'parent_id' => $parentId,
'school_year' => $schoolYear,
'source_school_year' => $sourceSchoolYear,
'parent_name' => trim((string) ($parent['firstname'] ?? '') . ' ' . (string) ($parent['lastname'] ?? '')) ?: 'Parent #' . $parentId,
'parent_email' => (string) ($parent['email'] ?? ''),
'students' => $students,
];
}
private function studentLinkedToParent(int $studentId, int $parentId): bool
{
if ($studentId <= 0 || $parentId <= 0 || ! $this->db->tableExists('students')) {
return false;
}
return $this->db->table('students')
->where('id', $studentId)
->where('parent_id', $parentId)
->countAllResults() > 0;
}
private function previousSchoolYearName(string $schoolYear): ?string
{
$schoolYear = trim($schoolYear);
if (! preg_match('/^(\d{4})-(\d{4})$/', $schoolYear, $matches)) {
return null;
}
return ((int) $matches[1] - 1) . '-' . ((int) $matches[2] - 1);
}
private function parentIdForStudent(int $studentId, string $schoolYear): int
{
if ($studentId <= 0) {
return 0;
}
if ($schoolYear !== '' && $this->db->tableExists('enrollments')) {
$row = $this->db->table('enrollments')
->select('parent_id')
->where('student_id', $studentId)
->where('school_year', $schoolYear)
->orderBy('updated_at', 'DESC')
->orderBy('id', 'DESC')
->limit(1)
->get()
->getRowArray();
if ((int) ($row['parent_id'] ?? 0) > 0) {
return (int) $row['parent_id'];
}
}
$student = $this->db->table('students')
->select('parent_id')
->where('id', $studentId)
->limit(1)
->get()
->getRowArray();
return (int) ($student['parent_id'] ?? 0);
}
private function ruleCodesForFlag(string $flagType): array
{
return match ($flagType) {
'AGE_EXCEPTION_REQUIRED' => ['AGE_RULE_BLOCKED'],
'LATE_REGISTRATION_EXCEPTION' => ['REGISTRATION_CLOSED'],
'FINANCIAL_REVIEW_REQUIRED' => ['OUTSTANDING_BALANCE_BLOCKED', 'FINANCE_APPROVAL_REQUIRED'],
'CLASS_CAPACITY_EXCEPTION_REQUIRED' => ['CLASS_CAPACITY_EXCEPTION_REQUIRED'],
'RESTRICTED_ADMINISTRATIVE_REVIEW' => ['EXPELLED'],
'WITHDRAWAL_REVIEW_REQUIRED' => ['WITHDRAWN'],
'DEFERRED_DELIBERATION' => ['NO_FINAL_DECISION', 'UNRECOGNIZED_DECISION', 'DEFERRED_DECISION'],
'SIBLING_LAST_NAME_MISMATCH' => ['SIBLING_LAST_NAME_MISMATCH'],
default => [$flagType],
};
}
private function canManageEnrollmentExceptions(): bool
{
$userId = $this->userId();
if ($userId <= 0) {
return false;
}
if (! $this->db->tableExists('permissions') || ! $this->db->tableExists('role_permissions') || ! $this->db->tableExists('user_roles')) {
return $this->isAdministratorSessionRole();
}
$rows = $this->db->table('user_roles ur')
->select('rp.*')
->join('role_permissions rp', 'rp.role_id = ur.role_id')
->join('permissions p', 'p.id = rp.permission_id')
->where('ur.user_id', $userId)
->where('LOWER(p.name)', 'enrollment.exception.manage')
->get()
->getResultArray();
foreach ($rows as $row) {
if (! empty($row['can_create']) || ! empty($row['can_update']) || ! empty($row['can_delete'])) {
return true;
}
}
return false;
}
private function isAdministratorSessionRole(): bool
{
$roles = array_map(static fn ($role): string => strtolower(trim((string) $role)), (array) session()->get('roles'));
$activeRole = strtolower(trim((string) session()->get('role')));
if ($activeRole !== '') {
$roles[] = $activeRole;
}
return (bool) array_intersect(array_unique($roles), ['administrator', 'principal']);
}
private function launchState(string $schoolYear): array
{
if ($schoolYear === '' || ! $this->db->tableExists('school_years')) {
@@ -579,6 +1185,20 @@ class EnrollmentAdminController extends BaseController
return is_numeric($row['parent_id'] ?? null) ? (int) $row['parent_id'] : null;
}
private function openFlagCount(string $schoolYear): int
{
if (! $this->db->tableExists('enrollment_flags')) {
return 0;
}
$builder = $this->db->table('enrollment_flags')->where('status', 'open');
if ($schoolYear !== '') {
$builder->where('school_year', $schoolYear);
}
return $builder->countAllResults();
}
private function flagTypes(): array
{
if (! $this->db->tableExists('enrollment_flags')) {