db = \Config\Database::connect(); } public function dashboard() { $schoolYear = trim((string) ($this->request->getGet('school_year') ?? $this->currentSchoolYearName((string) ($this->schoolYear ?? '')))); $status = trim((string) ($this->request->getGet('status') ?? 'open')); $flagType = trim((string) ($this->request->getGet('flag_type') ?? '')); $assignedTo = trim((string) ($this->request->getGet('assigned_to') ?? '')); $flags = $this->enrollmentFlags($schoolYear, $status, $flagType, $assignedTo); return view('administrator/enrollment_admin_dashboard', [ 'flags' => $flags, 'schoolYear' => $schoolYear, 'status' => $status, 'flagType' => $flagType, 'assignedTo' => $assignedTo, 'flagTypes' => $this->flagTypes(), 'schoolYears' => $this->schoolYears(), 'classSections' => $this->classSections($schoolYear), 'admins' => $this->adminUsers(), 'enrollmentFollowups' => $this->enrollmentFollowups($schoolYear), 'auditRows' => $this->auditRows($schoolYear), 'launchState' => $this->launchState($schoolYear), 'previewParentId' => $this->firstParentWithStudents(), 'emailExamples' => service('enrollmentRegistrationEmail')->previewExamplesForSchoolYear($schoolYear), ]); } public function approveLaunch() { $schoolYear = trim((string) ($this->request->getPost('school_year') ?? '')); if ($schoolYear === '') { return redirect()->back()->with('error', 'School year is required.'); } if (! $this->launchConfigurationComplete($schoolYear, $missing)) { return redirect()->back()->with('error', 'Registration launch is not ready: ' . implode(', ', $missing)); } $this->db->table('school_years') ->where('name', $schoolYear) ->update([ 'registration_launch_approved_at' => date('Y-m-d H:i:s'), 'registration_launch_approved_by' => $this->userId(), 'registration_email_template_version' => \App\Services\EnrollmentRegistrationEmailService::TEMPLATE_VERSION, 'updated_at' => date('Y-m-d H:i:s'), ]); return redirect()->back()->with('success', 'Registration launch approved for ' . $schoolYear . '.'); } public function sendRegistrationEmails() { $schoolYear = trim((string) ($this->request->getPost('school_year') ?? '')); if ($schoolYear === '') { return redirect()->back()->with('error', 'School year is required.'); } if (! $this->launchConfigurationComplete($schoolYear, $missing)) { return redirect()->back()->with('error', 'Registration launch is not ready: ' . implode(', ', $missing)); } $force = (bool) $this->request->getPost('force_resend'); $result = service('enrollmentRegistrationEmail')->sendForSchoolYearName($schoolYear, $force); $message = sprintf( 'Registration emails processed for %s: %d sent, %d failed, %d skipped.', $schoolYear, (int) ($result['sent'] ?? 0), (int) ($result['failed'] ?? 0), (int) ($result['skipped'] ?? 0) ); $details = array_filter(array_map('strval', $result['messages'] ?? [])); if ($details !== []) { $message .= ' ' . implode(' ', $details); } return redirect()->back()->with(((int) ($result['failed'] ?? 0) > 0) ? 'error' : 'success', $message); } public function previewEmail() { $schoolYear = trim((string) ($this->request->getGet('school_year') ?? '')); $parentId = (int) ($this->request->getGet('parent_id') ?? 0); if ($schoolYear === '' || $parentId <= 0) { return redirect()->back()->with('error', 'School year and parent are required for preview.'); } $message = service('enrollmentRegistrationEmail')->previewForParent($schoolYear, $parentId); if ($message === null) { return redirect()->back()->with('error', 'No preview email could be generated for that parent.'); } return view('administrator/enrollment_email_preview', [ 'schoolYear' => $schoolYear, 'parentId' => $parentId, 'subject' => $message['subject'], 'body' => $message['body'], ]); } public function resolveFlag(int $id) { try { $flag = $this->requireFlag($id); $notes = trim((string) ($this->request->getPost('resolution_notes') ?? '')); if ($notes === '') { return redirect()->back()->with('error', 'Resolution notes are required.'); } $this->resolveFlagRow($flag, $notes, 'flag_resolved'); return redirect()->back()->with('success', 'Enrollment flag resolved.'); } catch (Throwable $e) { return redirect()->back()->with('error', $e->getMessage()); } } public function assignClass(int $id) { try { $flag = $this->requireFlag($id); $sectionId = (int) ($this->request->getPost('class_section_id') ?? 0); $notes = trim((string) ($this->request->getPost('resolution_notes') ?? '')); if ($sectionId <= 0) { return redirect()->back()->with('error', 'Select a target class section.'); } if ($notes === '') { return redirect()->back()->with('error', 'Resolution notes are required.'); } $this->applyClassSection((int) $flag['student_id'], (string) $flag['school_year'], $sectionId, 'manual_class_assignment'); $this->resolveFlagRow($flag, $notes, 'manual_class_assignment', ['class_section_id' => $sectionId]); return redirect()->back()->with('success', 'Class assignment applied.'); } catch (Throwable $e) { return redirect()->back()->with('error', $e->getMessage()); } } public function confirmMakeupPromotion(int $id) { try { $flag = $this->requireFlag($id); if ((string) ($flag['flag_type'] ?? '') !== 'PENDING_MAKE_UP_EXAM_PROMOTION') { return redirect()->back()->with('error', 'This flag is not a make-up exam promotion flag.'); } $sectionId = (int) ($this->request->getPost('class_section_id') ?? 0); $examResult = trim((string) ($this->request->getPost('exam_result') ?? '')); $notes = trim((string) ($this->request->getPost('resolution_notes') ?? '')); if (! in_array($examResult, ['passed', 'failed'], true)) { return redirect()->back()->with('error', 'Select whether the make-up exam was passed or failed.'); } if ($notes === '') { return redirect()->back()->with('error', 'Resolution notes are required.'); } if ($examResult === 'passed') { if ($sectionId <= 0) { return redirect()->back()->with('error', 'Select the promoted class section.'); } $this->applyClassSection((int) $flag['student_id'], (string) $flag['school_year'], $sectionId, 'make_up_exam_promotion_completed', 'promotion_completed'); $this->resolveFlagRow($flag, $notes, 'make_up_exam_promotion_completed', [ 'exam_result' => $examResult, 'class_section_id' => $sectionId, ]); } else { $this->updateEnrollmentPlacementStatus((int) $flag['student_id'], (string) $flag['school_year'], 'no_promotion_required'); $this->resolveFlagRow($flag, $notes, 'make_up_exam_no_promotion_required', [ 'exam_result' => $examResult, ]); } return redirect()->back()->with('success', 'Make-up exam follow-up resolved.'); } catch (Throwable $e) { return redirect()->back()->with('error', $e->getMessage()); } } public function approveException(int $id) { try { $flag = $this->requireFlag($id); $reason = trim((string) ($this->request->getPost('reason') ?? '')); if ($reason === '') { return redirect()->back()->with('error', 'Approval reason is required.'); } $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'), ]); $this->resolveFlagRow($flag, $reason, 'enrollment_exception_approved'); return redirect()->back()->with('success', 'Enrollment exception approved.'); } catch (Throwable $e) { return redirect()->back()->with('error', $e->getMessage()); } } private function enrollmentFlags(string $schoolYear, string $status, string $flagType, string $assignedTo): array { if (! $this->db->tableExists('enrollment_flags')) { return []; } $builder = $this->db->table('enrollment_flags ef') ->select('ef.*') ->select('s.firstname, s.lastname, s.school_id') ->select('u.firstname AS assignee_firstname, u.lastname AS assignee_lastname') ->join('students s', 's.id = ef.student_id', 'left') ->join('users u', 'u.id = ef.assigned_to', 'left') ->orderBy('ef.created_at', 'DESC') ->orderBy('ef.id', 'DESC'); if ($schoolYear !== '') { $builder->where('ef.school_year', $schoolYear); } if ($status !== '') { $builder->where('ef.status', $status); } if ($flagType !== '') { $builder->where('ef.flag_type', $flagType); } if (is_numeric($assignedTo) && (int) $assignedTo > 0) { $builder->where('ef.assigned_to', (int) $assignedTo); } $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['assignee_name'] = trim((string) ($row['assignee_firstname'] ?? '') . ' ' . (string) ($row['assignee_lastname'] ?? '')); $row['details'] = json_decode((string) ($row['details_json'] ?? ''), true) ?: []; } unset($row); return $rows; } private function enrollmentFollowups(string $schoolYear): array { if (! $this->db->tableExists('enrollments')) { return []; } $fields = $this->db->getFieldNames('enrollments'); $select = [ 'e.id', 'e.student_id', 'e.school_year', 'e.enrollment_status', 'e.class_section_id', 'e.updated_at', 's.firstname', 's.lastname', 's.school_id', 'cs.class_section_name', ]; foreach ([ 'deliberation_decision', 'placement_status', 'exception_required', 'exception_reason', 'source_school_year', 'assigned_class_section_id', 'age_on_reference_date', ] as $field) { if (in_array($field, $fields, true)) { $select[] = 'e.' . $field; } } $builder = $this->db->table('enrollments e') ->select(implode(', ', $select)) ->join('students s', 's.id = e.student_id', 'left') ->join('classSection cs', 'cs.class_section_id = e.class_section_id', 'left') ->orderBy('e.updated_at', 'DESC') ->orderBy('e.id', 'DESC') ->limit(200); if ($schoolYear !== '') { $builder->where('e.school_year', $schoolYear); } $builder->groupStart() ->whereIn('e.enrollment_status', [ 'review & decision', 'admission under review', 'waitlist', 'denied', 'Review & Decision', 'Admission Under Review', 'Waitlist', 'Denied', ]); if (in_array('placement_status', $fields, true)) { $builder->orWhereIn('e.placement_status', [ 'temporary_same_grade', 'temporary_manual_class_required', 'manual_class_required', 'exit_required', 'automatic_distribution_pending', ]); } if (in_array('exception_required', $fields, true)) { $builder->orWhere('e.exception_required', 1); } if (in_array('deliberation_decision', $fields, true)) { $builder->orWhereIn('e.deliberation_decision', [ 'make_up_exam', 'repeat_class', 'deferred', 'expelled', 'withdrawn', ]); } $rows = $builder->groupEnd()->get()->getResultArray(); foreach ($rows as &$row) { $row['student_name'] = trim((string) ($row['firstname'] ?? '') . ' ' . (string) ($row['lastname'] ?? '')) ?: 'Student #' . (int) ($row['student_id'] ?? 0); } unset($row); return $rows; } private function requireFlag(int $id): array { if ($id <= 0 || ! $this->db->tableExists('enrollment_flags')) { throw new \RuntimeException('Enrollment flag was not found.'); } $flag = $this->db->table('enrollment_flags')->where('id', $id)->limit(1)->get()->getRowArray(); if ($flag === null) { throw new \RuntimeException('Enrollment flag was not found.'); } return $flag; } private function resolveFlagRow(array $flag, string $notes, string $auditAction, array $metadata = []): void { $this->db->transStart(); $original = $flag; $this->db->table('enrollment_flags') ->where('id', (int) $flag['id']) ->update([ 'status' => 'resolved', 'resolved_at' => date('Y-m-d H:i:s'), 'resolution_notes' => $notes, ]); $this->audit((int) $flag['student_id'], (string) $flag['school_year'], (string) ($flag['source_school_year'] ?? ''), $auditAction, $original, array_merge($metadata, [ 'flag_id' => (int) $flag['id'], 'flag_type' => (string) $flag['flag_type'], 'resolution_notes' => $notes, ]), $notes); $this->db->transComplete(); if ($this->db->transStatus() === false) { throw new \RuntimeException('Unable to resolve enrollment flag.'); } } private function applyClassSection(int $studentId, string $schoolYear, int $sectionId, string $auditAction, string $placementStatus = 'manual_class_assigned'): void { $section = $this->db->table('classSection') ->select('class_section_id, class_id, class_section_name') ->where('class_section_id', $sectionId) ->orderBy('id', 'DESC') ->limit(1) ->get() ->getRowArray(); if ($section === null) { throw new \RuntimeException('Selected class section was not found.'); } $originalEnrollment = $this->latestEnrollment($studentId, $schoolYear); $payload = [ 'class_section_id' => $sectionId, 'assigned_class_section_id' => $sectionId, 'assigned_grade_id' => (int) ($section['class_id'] ?? 0) ?: null, 'placement_status' => $placementStatus, 'updated_at' => date('Y-m-d H:i:s'), ]; $this->db->table('enrollments') ->where('student_id', $studentId) ->where('school_year', $schoolYear) ->update($payload); $studentClass = $this->db->table('student_class') ->where('student_id', $studentId) ->where('school_year', $schoolYear) ->orderBy('id', 'DESC') ->limit(1) ->get() ->getRowArray(); $studentClassPayload = [ 'student_id' => $studentId, 'class_section_id' => $sectionId, 'school_year' => $schoolYear, 'updated_by' => $this->userId(), 'updated_at' => date('Y-m-d H:i:s'), ]; if ($studentClass !== null) { $this->db->table('student_class')->where('id', (int) $studentClass['id'])->update($studentClassPayload); } else { $studentClassPayload['created_at'] = date('Y-m-d H:i:s'); $this->db->table('student_class')->insert($studentClassPayload); } $this->audit($studentId, $schoolYear, (string) ($originalEnrollment['source_school_year'] ?? ''), $auditAction, $originalEnrollment, $payload, 'Class section assigned by administrator.'); } private function updateEnrollmentPlacementStatus(int $studentId, string $schoolYear, string $placementStatus): void { $this->db->table('enrollments') ->where('student_id', $studentId) ->where('school_year', $schoolYear) ->update([ 'placement_status' => $placementStatus, 'updated_at' => date('Y-m-d H:i:s'), ]); } private function latestEnrollment(int $studentId, string $schoolYear): ?array { return $this->db->table('enrollments') ->where('student_id', $studentId) ->where('school_year', $schoolYear) ->orderBy('updated_at', 'DESC') ->orderBy('id', 'DESC') ->limit(1) ->get() ->getRowArray() ?: null; } private function audit(int $studentId, string $schoolYear, string $sourceSchoolYear, string $action, ?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' => $sourceSchoolYear !== '' ? $sourceSchoolYear : null, 'action' => $action, 'performed_by' => $this->userId(), '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' => date('Y-m-d H:i:s'), ]); } private function auditRows(string $schoolYear): array { if (! $this->db->tableExists('enrollment_transition_audits')) { return []; } $builder = $this->db->table('enrollment_transition_audits eta') ->select('eta.*') ->select('s.firstname, s.lastname') ->select('u.firstname AS user_firstname, u.lastname AS user_lastname') ->join('students s', 's.id = eta.student_id', 'left') ->join('users u', 'u.id = eta.performed_by', 'left') ->orderBy('eta.created_at', 'DESC') ->limit(50); if ($schoolYear !== '') { $builder->where('eta.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['performed_by_name'] = trim((string) ($row['user_firstname'] ?? '') . ' ' . (string) ($row['user_lastname'] ?? '')) ?: ((int) ($row['performed_by'] ?? 0) > 0 ? 'User #' . (int) $row['performed_by'] : ''); } unset($row); return $rows; } private function launchState(string $schoolYear): array { if ($schoolYear === '' || ! $this->db->tableExists('school_years')) { return ['approved' => false, 'approved_at' => null, 'missing' => ['school year']]; } $row = $this->db->table('school_years')->where('name', $schoolYear)->limit(1)->get()->getRowArray(); $this->launchConfigurationComplete($schoolYear, $missing); return [ 'approved' => ! empty($row['registration_launch_approved_at'] ?? null), 'approved_at' => $row['registration_launch_approved_at'] ?? null, 'missing' => $missing, ]; } private function launchConfigurationComplete(string $schoolYear, ?array &$missing = null): bool { $missing = []; $row = $this->db->table('school_years')->where('name', $schoolYear)->limit(1)->get()->getRowArray(); if ($row === null) { $missing[] = 'school year record'; return false; } foreach (['registration_starts_on' => 'registration opening date', 'registration_ends_on' => 'registration deadline'] as $field => $label) { if (empty($row[$field])) { $missing[] = $label; } } if (! $this->db->tableExists('email_templates')) { $missing[] = 'email templates table'; } else { $fields = $this->db->getFieldNames('email_templates'); $keyField = in_array('code', $fields, true) ? 'code' : 'template_key'; $template = $this->db->table('email_templates') ->where($keyField, 'registration_opening') ->where('is_active', 1) ->countAllResults(); if ($template <= 0) { $missing[] = 'approved registration email template'; } } return $missing === []; } private function firstParentWithStudents(): ?int { if (! $this->db->tableExists('students')) { return null; } $row = $this->db->table('students') ->select('parent_id') ->where('parent_id IS NOT NULL', null, false) ->orderBy('parent_id', 'ASC') ->limit(1) ->get() ->getRowArray(); return is_numeric($row['parent_id'] ?? null) ? (int) $row['parent_id'] : null; } private function flagTypes(): array { if (! $this->db->tableExists('enrollment_flags')) { return []; } return array_column($this->db->table('enrollment_flags')->select('flag_type')->distinct()->orderBy('flag_type')->get()->getResultArray(), 'flag_type'); } private function schoolYears(): array { if (! $this->db->tableExists('school_years')) { return []; } return $this->db->table('school_years')->select('name')->orderBy('name', 'DESC')->get()->getResultArray(); } private function classSections(string $schoolYear): array { if (! $this->db->tableExists('classSection')) { return []; } $builder = $this->db->table('classSection')->select('class_section_id, class_section_name')->orderBy('class_section_name', 'ASC'); if ($schoolYear !== '' && $this->db->fieldExists('school_year', 'classSection')) { $builder->where('school_year', $schoolYear); } return $builder->get()->getResultArray(); } private function adminUsers(): array { if (! $this->db->tableExists('users')) { return []; } return $this->db->table('users') ->select('id, firstname, lastname, user_type') ->whereIn('user_type', ['administrator', 'admin', 'principal', 'administrative staff']) ->orderBy('lastname', 'ASC') ->get() ->getResultArray(); } private function userId(): ?int { $id = session('user_id') ?? session('id'); return is_numeric($id) ? (int) $id : null; } }