rolePermissionModel = new RolePermissionModel(); $this->permissionModel = new PermissionModel(); $this->roleModel = new RoleModel(); $this->userModel = new UserModel(); $this->configModel = new ConfigurationModel(); $this->studentModel = new StudentModel(); $this->loginActivityModel = new LoginActivityModel(); $this->userRoleModel = new UserRoleModel(); $this->invoiceModel = new InvoiceModel(); $this->refundModel = new RefundModel(); $this->enrollmentModel = new EnrollmentModel(); $this->classSectionModel = new ClassSectionModel(); $this->adminNotificationSubjectModel = new AdminNotificationSubjectModel(); $this->semester = $this->configModel->getConfig('semester'); $this->schoolYear = $this->configModel->getConfig('school_year'); $this->studentClassModel = new StudentClassModel(); $this->staffAttendanceModel = new StaffAttendanceModel(); // Load the database service $this->db = \Config\Database::connect(); // Check if the database connection is established if (!$this->db->connect()) { log_message('error', 'Database connection failed.'); throw new \Exception('Database connection failed.'); } else { log_message('info', 'Database connection successful.'); } } /** * Compute all allowed absence dates (future Sundays within Sep..May for the configured school year) */ private function allowedAbsenceDates(): array { $todayStr = local_date(utc_now(), 'Y-m-d'); try { $today = new \DateTimeImmutable($todayStr); } catch (\Throwable $e) { $today = new \DateTimeImmutable(); } $sy = (string)($this->schoolYear ?? ''); $startYear = null; $endYear = null; if (preg_match('/^(\d{4})\D+(\d{4})$/', $sy, $m)) { $startYear = (int)$m[1]; $endYear = (int)$m[2]; } else { $cy = (int)date('Y'); $cm = (int)date('n'); if ($cm >= 9) { // Sep-Dec $startYear = $cy; $endYear = $cy + 1; } else { $startYear = $cy - 1; $endYear = $cy; } } try { $start = new \DateTimeImmutable(sprintf('%04d-09-01', $startYear)); $end = new \DateTimeImmutable(sprintf('%04d-05-31', $endYear)); } catch (\Throwable $e) { return []; } if ($start < $today) { $start = $today; } $dates = []; for ($cursor = $start; $cursor <= $end; $cursor = $cursor->modify('+1 day')) { if ((int)$cursor->format('w') === 0) { // Sunday $dates[] = $cursor->format('Y-m-d'); } } return $dates; } private function getPreviousSchoolYear(string $schoolYear): string { $schoolYear = trim($schoolYear); if ($schoolYear === '') { return ''; } if (preg_match('/^(\d{4})\s*-\s*(\d{4})$/', $schoolYear, $m)) { return ((int)$m[1] - 1) . '-' . ((int)$m[2] - 1); } if (preg_match('/^(\d{4})\s*-\s*(\d{2})$/', $schoolYear, $m)) { $start = (int)$m[1] - 1; $end = (int)$m[2] - 1; if ($end < 0) { $end += 100; } return sprintf('%04d-%02d', $start, $end); } if (preg_match('/^\d{4}$/', $schoolYear)) { return (string)((int)$schoolYear - 1); } return ''; } private function getSchoolYearStartYear(string $schoolYear): ?int { $schoolYear = trim($schoolYear); if ($schoolYear === '') { return null; } if (preg_match('/^(\d{4})\s*-\s*(\d{4})$/', $schoolYear, $m)) { return (int)$m[1]; } if (preg_match('/^(\d{4})\s*-\s*(\d{2})$/', $schoolYear, $m)) { return (int)$m[1]; } if (preg_match('/^\d{4}$/', $schoolYear)) { return (int)$schoolYear; } return null; } /** * Admin self-service absence/vacation page (same features as teacher page) */ public function absenceFormAdmin() { $userId = (int)(session()->get('user_id') ?? 0); if ($userId <= 0) { return redirect()->to('login')->with('error', 'Please log in first.'); } $admin = $this->userModel->find($userId); $displayName = $admin ? trim(($admin['firstname'] ?? '') . ' ' . ($admin['lastname'] ?? '')) : 'Administrator'; $semester = (string)($this->semester ?? ''); $schoolYear = (string)($this->schoolYear ?? ''); $existing = $this->staffAttendanceModel ->where('user_id', $userId) ->where('semester', $semester) ->where('school_year', $schoolYear) ->orderBy('date', 'DESC') ->findAll(); return view('administrator/absence_vacation', [ 'admin_name' => $displayName, 'semester' => $semester, 'schoolYear' => $schoolYear, 'existing' => $existing, 'availableDates' => $this->allowedAbsenceDates(), ]); } /** * Handle admin absence submission (upserts staff_attendance like the teacher flow) */ public function submitAbsenceAdmin() { $userId = (int)(session()->get('user_id') ?? 0); if ($userId <= 0) { return redirect()->to('login')->with('error', 'Please log in first.'); } $semester = (string)($this->semester ?? ''); $schoolYear = (string)($this->schoolYear ?? ''); if ($schoolYear === '') { return redirect()->to('/administrator/absence')->with('status', 'error')->with('message', 'Semester or school year not configured.'); } $semesterResolver = new SemesterRangeService($this->configModel); $dates = (array)($this->request->getPost('dates') ?? []); $reasonType = trim((string)($this->request->getPost('reason_type') ?? '')); $reasonText = trim((string)($this->request->getPost('reason') ?? '')); if ($reasonText === '') { return redirect()->to('/administrator/absence') ->with('status', 'error') ->with('message', 'Reason is required.'); } $reasonBase = ($reasonType !== '') ? strtolower($reasonType) : ''; $reason = $reasonBase !== '' ? ($reasonBase . ': ' . $reasonText) : $reasonText; $allowedDates = $this->allowedAbsenceDates(); $allowedSet = array_fill_keys($allowedDates, true); $roleName = $this->userModel->getUserRole($userId) ?: null; $saved = 0; $invalid = []; $savedDates = []; $dates = array_values(array_unique(array_map('strval', $dates))); foreach ($dates as $d) { $d = trim((string)$d); if ($d === '') continue; $dt = date_create_from_format('Y-m-d', $d); if (!$dt || $dt->format('Y-m-d') !== $d || empty($allowedSet[$d])) { $invalid[] = $d; continue; } $semesterForDate = $semesterResolver->getSemesterForDate($d); if ($semesterForDate === '') { $semesterForDate = $semester; } $ok = $this->staffAttendanceModel->upsertOne( userId: $userId, roleName: $roleName, date: $d, semester: $semesterForDate, schoolYear: $schoolYear, status: StaffAttendanceModel::STATUS_ABSENT, reason: $reason, editorId: $userId ); if ($ok) { $saved++; $savedDates[] = $d; } } if (!empty($invalid)) { return redirect()->to('/administrator/absence') ->with('status', 'error') ->with('message', 'Invalid dates: ' . implode(', ', $invalid)); } // Send notification email to principal with request details try { $user = $this->userModel->find($userId) ?: []; $fullName = trim(($user['firstname'] ?? '') . ' ' . ($user['lastname'] ?? '')) ?: 'Administrator'; $userEmail = $user['email'] ?? ''; $role = $roleName ?: 'admin'; $dateList = !empty($savedDates) ? implode(', ', $savedDates) : implode(', ', $dates); $subject = sprintf('TimeOff Request: %s (%s) — %s', $fullName, ucfirst((string)$role), $dateList ?: 'No dates'); $submittedAt = utc_now(); $body = '
' . '

New TimeOff Request

' . '

A staff time-off request was submitted from the administrator portal.

' . '' . '' . '' . '' . '' . '' . '' . '' . '' . '' . '
Name' . esc($fullName) . '
Email' . esc($userEmail) . '
Role' . esc((string)$role) . '
Semester' . esc($semester) . '
School Year' . esc($schoolYear) . '
Reason Type' . esc($reasonType ?: '-') . '
Reason' . esc($reasonText) . '
Dates' . esc($dateList ?: '-') . '
Submitted At' . esc($submittedAt) . '
' . '
'; $linkService = new StaffTimeOffLinkService(); $token = $linkService->createToken([ 'uid' => $userId, 'email' => $userEmail, 'name' => $fullName, 'role' => $role, 'dates' => $dateList ?: '-', 'reason' => $reasonText, 'reason_type' => $reasonType, 'submitted_at' => $submittedAt, 'origin' => 'administrator portal', ]); $notifyUrl = base_url('timeoff/notify/' . rawurlencode($token)); $body .= '

Click Send confirmation email to ' . esc($fullName) . ' so the staff member is notified automatically. ' . 'This link expires in 14 days.

'; $mailer = \Config\Services::emailService(); $principalEmail = env('PRINCIPAL_EMAIL') ?: 'principal@alrahmaisgl.org'; $mailer->send($principalEmail, $subject, $body, 'notifications'); } catch (\Throwable $e) { log_message('error', 'Failed to send TimeOff email (admin): ' . $e->getMessage()); } return redirect()->to('/administrator/absence') ->with('status', 'success') ->with('message', $saved . ' day(s) saved as absent.'); } public function index() { $data = [ 'users' => $this->userModel->findAll(), 'roles' => $this->roleModel->findAll(), ]; return view('administrator/dashboard', $data); } public function isadministrator() { $session = session(); $allowedRoles = [ 'administrator', 'admin', 'principal', //'vice_principal', //'vice principal', ]; $role = strtolower((string) $session->get('role')); if ($role !== '' && in_array($role, $allowedRoles, true)) { return true; } $roles = $session->get('roles'); if (is_array($roles)) { foreach ($roles as $candidate) { $candidate = strtolower((string) $candidate); if ($candidate !== '' && in_array($candidate, $allowedRoles, true)) { return true; } } } return false; } public function administratorDashboard() { helper('url'); $searchData = $this->buildUserSearchData((string) $this->request->getGet('query')); return view('administrator/administratordashboard', array_merge($searchData, [ 'dashboardEndpoint' => site_url('api/administrator/dashboard'), ])); } public function dashboardMetrics() { return $this->response->setJSON($this->buildDashboardMetrics()); } private function buildDashboardMetrics(): array { $recentActivities = $this->loginActivityModel->getLastActivities(4); if (!is_array($recentActivities)) { $recentActivities = []; } $totalAdmins = (int) ($this->userModel->countAdminsBySchoolYear($this->schoolYear) ?? 0); $teachers = $this->userModel->getUsersByRoleAndSchoolYear('teacher', $this->schoolYear); $totalTeachers = $this->countUniqueEntities($teachers); $teacherAssistants = $this->userModel->getUsersByRoleAndSchoolYear('teacher_assistant', $this->schoolYear); $totalTeacherAssistants = $this->countUniqueEntities($teacherAssistants); $parents = $this->userModel->getUsersByRoleAndSchoolYear('parent', $this->schoolYear); $totalParents = $this->countUniqueEntities($parents); // Count only students that have a class assigned and exist in student_class for the current school year $totalStudents = (int) ( $this->db->table('student_class') ->select('COUNT(DISTINCT student_class.student_id) AS cnt') ->where('student_class.school_year', $this->schoolYear) ->where('student_class.class_section_id IS NOT NULL', null, false) ->get() ->getRow('cnt') ?? 0 ); return [ 'counts' => [ 'students' => $totalStudents, 'teachers' => $totalTeachers, 'teacherAssistants' => $totalTeacherAssistants, 'admins' => $totalAdmins, 'parents' => $totalParents, ], 'recentActivities' => array_map(static function ($activity) { if (!is_array($activity)) { return []; } return [ 'login_time' => $activity['login_time'] ?? null, 'email' => $activity['email'] ?? null, ]; }, $recentActivities), 'meta' => [ 'schoolYear' => $this->schoolYear, 'semester' => $this->semester, ], ]; } private function countUniqueEntities($rows): int { if (!is_array($rows) || $rows === []) { return 0; } $ids = []; foreach ($rows as $row) { if (!is_array($row)) { continue; } if (isset($row['id'])) { $ids[] = (int) $row['id']; continue; } if (isset($row['user_id'])) { $ids[] = (int) $row['user_id']; } } return count(array_unique($ids)); } public function userSearch() { $data = $this->buildUserSearchData((string) $this->request->getGet('query')); return view('administrator/search_results', $data); } private function buildUserSearchData(string $query): array { $q = trim($query); if ($q === '') { return [ 'query' => '', 'results' => [], 'scope_used' => 'unscoped-raw', 'scope_label' => 'all years/semesters (raw)', 'total_found' => 0, ]; } $db = $this->db; // 1) Tokenize input: split by whitespace and punctuation, keep meaningful pieces $rawTokens = preg_split('/[,\s]+/u', $q, -1, PREG_SPLIT_NO_EMPTY) ?: []; $tokens = array_values(array_filter(array_map('trim', $rawTokens))); // 2) Build phone variants for any token that looks numeric-ish $phoneMap = []; // token => variants[] foreach ($tokens as $t) { $digits = preg_replace('/\D+/', '', $t); if ($digits === '') { continue; } $v = []; if (strlen($digits) >= 7) { // base forms $v[] = $digits; if (strlen($digits) === 10) { $v[] = sprintf('(%s)-%s-%s', substr($digits, 0, 3), substr($digits, 3, 3), substr($digits, 6)); $v[] = sprintf('%s-%s-%s', substr($digits, 0, 3), substr($digits, 3, 3), substr($digits, 6)); $v[] = sprintf('%s %s %s', substr($digits, 0, 3), substr($digits, 3, 3), substr($digits, 6)); // country-code forms $v[] = '1' . $digits; $v[] = '+1' . $digits; $v[] = '+1 ' . sprintf('(%s) %s-%s', substr($digits, 0, 3), substr($digits, 3, 3), substr($digits, 6)); $v[] = '+1-' . sprintf('%s-%s-%s', substr($digits, 0, 3), substr($digits, 3, 3), substr($digits, 6)); } elseif (strlen($digits) === 11 && str_starts_with($digits, '1')) { $ten = substr($digits, 1); $v[] = $ten; $v[] = sprintf('(%s)-%s-%s', substr($ten, 0, 3), substr($ten, 3, 3), substr($ten, 6)); $v[] = sprintf('%s-%s-%s', substr($ten, 0, 3), substr($ten, 3, 3), substr($ten, 6)); $v[] = sprintf('%s %s %s', substr($ten, 0, 3), substr($ten, 3, 3), substr($ten, 6)); $v[] = '+1' . $ten; $v[] = '+1 ' . sprintf('(%s) %s-%s', substr($ten, 0, 3), substr($ten, 3, 3), substr($ten, 6)); $v[] = '+1-' . sprintf('%s-%s-%s', substr($ten, 0, 3), substr($ten, 3, 3), substr($ten, 6)); } else { // 7-9 digits: keep as-is (partial phone fragment) $v[] = $digits; } } if (!empty($v)) { $phoneMap[$t] = array_values(array_unique($v)); } } // Helper: ( token1 AND token2 AND ... ), each token may match ANY of $columns, // and phone variants are only applied to the provided $phoneCols for THIS table. $applyMultiTokenLike = function ($qb, array $columns, array $tokens, array $phoneCols = []) use ($phoneMap) { foreach ($tokens as $t) { $qb->groupStart(); // OR across columns for this token foreach ($columns as $i => $col) { if ($i === 0) { $qb->like($col, $t); } else { $qb->orLike($col, $t); } } if (!empty($phoneMap[$t]) && !empty($phoneCols)) { foreach ($phoneMap[$t] as $pv) { foreach ($phoneCols as $pcol) { $qb->orLike($pcol, $pv); } } } $qb->groupEnd(); } return $qb; }; // ===== RAW UNscoped searches (flat arrays) ===== // USERS (phone col: cellphone) $uCols = ['firstname', 'lastname', 'email', 'cellphone', 'school_id', 'city', 'state']; $uQB = $db->table('users') ->select('id, firstname, lastname, email, cellphone, school_id, city, state, school_year, semester'); $applyMultiTokenLike($uQB, $uCols, $tokens, ['cellphone']); $users = $uQB->limit(150)->get()->getResultArray(); // STUDENTS (no phone column to search) $sCols = ['firstname', 'lastname', 'school_id', 'rfid_tag', 'dob', 'gender']; $sQB = $db->table('students') ->select('id, parent_id, school_id, firstname, lastname, dob, gender, school_year, semester, rfid_tag'); $applyMultiTokenLike($sQB, $sCols, $tokens, []); $students = $sQB->limit(150)->get()->getResultArray(); // PARENTS (phone col: secondparent_phone) $pCols = ['secondparent_firstname', 'secondparent_lastname', 'secondparent_email', 'secondparent_phone']; $pQB = $db->table('parents') ->select('id, firstparent_id, secondparent_firstname, secondparent_lastname, secondparent_email, secondparent_phone, school_year, semester'); $applyMultiTokenLike($pQB, $pCols, $tokens, ['secondparent_phone']); foreach ($tokens as $t) { if (ctype_digit($t)) { $pQB->orWhere('firstparent_id', (int) $t)->orWhere('id', (int) $t); } } $parents = $pQB->limit(150)->get()->getResultArray(); // STAFF (phone col: phone) $stCols = ['firstname', 'lastname', 'email', 'role_name', 'phone']; $stQB = $db->table('staff') ->select('id, user_id, firstname, lastname, email, phone, role_name, school_year, active_role'); $applyMultiTokenLike($stQB, $stCols, $tokens, ['phone']); $staff = $stQB->limit(150)->get()->getResultArray(); // EMERGENCY CONTACTS (phone col: cellphone) $ecCols = ['emergency_contact_name', 'relation', 'email', 'cellphone']; $ecQB = $db->table('emergency_contacts') ->select('id, parent_id, emergency_contact_name, relation, cellphone, email, school_year, semester'); $applyMultiTokenLike($ecQB, $ecCols, $tokens, ['cellphone']); $emergency = $ecQB->limit(150)->get()->getResultArray(); $raw = [ 'users' => $users, 'students' => $students, 'parents' => $parents, 'staff' => $staff, 'emergency_contacts' => $emergency, ]; $total = count($users) + count($students) + count($parents) + count($staff) + count($emergency); return [ 'query' => $q, 'results' => $raw, 'scope_used' => 'unscoped-raw', 'scope_label' => 'all years/semesters (raw, tokenized)', 'total_found' => $total, ]; } public function teachers() { return view('administrator/teachers'); // This is the correct view path } public function classes() { return view('administrator/classes'); } public function subjects() { return view('administrator/subjects'); } public function reports() { return view('administrator/reports'); } public function teacherDashboard() { return view('teacher/dashboard'); } public function parentDashboard() { return view('parent/dashboard'); } public function CourseMaterials() { return view('administrator/course_materials'); } public function administratorProfiles() { return view('administrator/administrator_profiles'); } public function payrollManagement() { return view('administrator/payroll_management'); } public function administratorAttendanceRecords() { return view('administrator/administrator_attendance_records'); } public function performanceReviews() { return view('administrator/performance_reviews'); } public function communicationLogs() { return view('administrator/communication_logs'); } public function feePaymentRecords() { return view('administrator/fee_payment_records'); } public function feedbackComplaints() { return view('administrator/feedback_complaints'); } public function teacherSubmissionsReport() { $semester = (string)($this->semester ?? ''); $schoolYear = (string)($this->schoolYear ?? ''); $scoreComments = new ScoreCommentModel(); $semesterScores = new SemesterScoreModel(); $attendanceDays = new AttendanceDayModel(); $historyModel = new TeacherSubmissionNotificationHistoryModel(); $assignmentRows = $this->db->table('teacher_class tc') ->select([ 'tc.class_section_id', 'cs.class_section_name', 'tc.teacher_id', 'u.firstname', 'u.lastname', 'tc.position', ]) ->join('classSection cs', 'cs.class_section_id = tc.class_section_id', 'left') ->join('users u', 'u.id = tc.teacher_id', 'left') ->where('tc.school_year', $schoolYear) ->where('tc.semester', $semester) ->orderBy('cs.class_section_name', 'ASC') ->get() ->getResultArray(); $teachersBySection = []; foreach ($assignmentRows as $assignment) { $sectionId = (int)($assignment['class_section_id'] ?? 0); if ($sectionId <= 0) { continue; } $positionKey = strtolower(trim((string)($assignment['position'] ?? ''))); $roleKey = $positionKey !== '' ? $positionKey : 'teacher'; $positionLabel = match ($roleKey) { 'ta' => 'TA', 'main' => 'Main', default => $roleKey !== '' ? ucfirst($roleKey) : 'Teacher', }; $teacherFullName = trim(($assignment['firstname'] ?? '') . ' ' . ($assignment['lastname'] ?? '')); $teacherId = (int)($assignment['teacher_id'] ?? 0); if ($teacherFullName === '' || $teacherId <= 0) { continue; } $entry = &$teachersBySection[$sectionId]; if (!isset($entry)) { $entry = [ 'class_section' => $assignment['class_section_name'] ?? "Section {$sectionId}", 'teachers' => [], ]; } $entry['teachers'][] = [ 'id' => $teacherId, 'label' => "{$positionLabel}: {$teacherFullName}", 'role_key' => $roleKey, ]; unset($entry); } $today = (new \DateTimeImmutable('now', new \DateTimeZone(date_default_timezone_get() ?: 'UTC')))->format('Y-m-d'); $rows = []; $totalStatuses = 0; $missingItemCount = 0; $allTeacherIds = []; $allClassSectionIds = []; foreach ($teachersBySection as $classSectionId => $section) { $classSectionId = (int)$classSectionId; if ($classSectionId <= 0) { continue; } $studentEntries = $this->studentClassModel ->select('student_id') ->where('class_section_id', $classSectionId) ->where('semester', $semester) ->where('school_year', $schoolYear) ->findAll(); $studentIds = array_filter(array_map(static fn($entry) => (int)($entry['student_id'] ?? 0), $studentEntries)); $expected = count($studentIds); $midtermStudents = []; $participationStudents = []; if ($classSectionId > 0) { $scoreRecords = $semesterScores ->where('class_section_id', $classSectionId) ->where('semester', $semester) ->where('school_year', $schoolYear) ->findAll(); foreach ($scoreRecords as $score) { $sid = (int)($score['student_id'] ?? 0); if ($sid <= 0 || ($expected > 0 && !in_array($sid, $studentIds, true))) { continue; } $midtermValue = trim((string)($score['midterm_exam_score'] ?? '')); if ($midtermValue !== '') { $midtermStudents[$sid] = true; } $participationValue = trim((string)($score['participation_score'] ?? '')); if ($participationValue !== '') { $participationStudents[$sid] = true; } } } $midtermCommentStudents = []; $ptapCommentStudents = []; if (!empty($studentIds)) { $comments = $scoreComments ->select('student_id, score_type, comment') ->whereIn('student_id', $studentIds) ->where('semester', $semester) ->where('school_year', $schoolYear) ->whereIn('score_type', ['midterm', 'ptap']) ->findAll(); foreach ($comments as $comment) { $sid = (int)($comment['student_id'] ?? 0); if ($sid <= 0) { continue; } $text = trim((string)($comment['comment'] ?? '')); if ($text === '') { continue; } $type = strtolower(trim((string)($comment['score_type'] ?? ''))); if ($type === 'midterm') { $midtermCommentStudents[$sid] = true; } if ($type === 'ptap') { $ptapCommentStudents[$sid] = true; } } } $attendanceRow = $attendanceDays ->where('class_section_id', $classSectionId) ->where('semester', $semester) ->where('school_year', $schoolYear) ->where('date', $today) ->first(); $attendanceSubmitted = $attendanceRow && in_array(strtolower((string)($attendanceRow['status'] ?? '')), ['submitted', 'published', 'finalized'], true); $teacherList = $section['teachers'] ?? []; if (!empty($teacherList)) { usort($teacherList, function ($a, $b) { return $this->teacherRolePriority($a['role_key'] ?? 'teacher') <=> $this->teacherRolePriority($b['role_key'] ?? 'teacher'); }); $teacherList = array_values($teacherList); } foreach ($teacherList as $teacherEntry) { if (!empty($teacherEntry['id'])) { $allTeacherIds[] = $teacherEntry['id']; } } $allClassSectionIds[] = $classSectionId; $midtermScoreStatus = $this->submissionStatus(count($midtermStudents), $expected); $midtermCommentStatus = $this->submissionStatus(count($midtermCommentStudents), $expected); $participationStatus = $this->submissionStatus(count($participationStudents), $expected); $ptapCommentStatus = $this->submissionStatus(count($ptapCommentStudents), $expected); $attendanceStatus = $this->attendanceStatus($attendanceSubmitted); $statusDetails = [ 'midterm_score_status' => $midtermScoreStatus, 'midterm_comment_status' => $midtermCommentStatus, 'participation_status' => $participationStatus, 'ptap_comment_status' => $ptapCommentStatus, ]; $missingItemsForSection = $this->buildMissingItems($statusDetails); $missingItemCount += count($missingItemsForSection); $totalStatuses += count($statusDetails); $rows[] = [ 'class_section' => $section['class_section'] ?? "Section {$classSectionId}", 'class_section_id' => $classSectionId, 'teachers' => $teacherList, 'midterm_score_status' => $midtermScoreStatus, 'midterm_comment_status' => $midtermCommentStatus, 'participation_status' => $participationStatus, 'ptap_comment_status' => $ptapCommentStatus, 'attendance_status' => $attendanceStatus, 'missing_items' => $missingItemsForSection, 'student_count' => $expected, ]; } $historyMap = []; $teacherIds = array_values(array_unique($allTeacherIds)); $classSectionIds = array_values(array_unique($allClassSectionIds)); if (!empty($teacherIds) && !empty($classSectionIds)) { $historyRecords = $historyModel ->select('teacher_submission_notification_history.*, u.firstname, u.lastname') ->join('users u', 'u.id = teacher_submission_notification_history.admin_id', 'left') ->where('notification_category', 'teacher_submissions') ->whereIn('teacher_submission_notification_history.teacher_id', $teacherIds) ->whereIn('teacher_submission_notification_history.class_section_id', $classSectionIds) ->orderBy('sent_at', 'DESC') ->findAll(); foreach ($historyRecords as $record) { $sectionId = (int)($record['class_section_id'] ?? 0); $teacherId = (int)($record['teacher_id'] ?? 0); if ($sectionId <= 0 || $teacherId <= 0) { continue; } $sentAt = $record['sent_at'] ?? null; $sentAtText = $sentAt ? local_datetime($sentAt, 'M j, Y g:i A') : ''; $adminName = trim(($record['firstname'] ?? '') . ' ' . ($record['lastname'] ?? '')); if ($adminName === '') { $adminName = 'Administrator'; } $historyMap[$sectionId][$teacherId][] = [ 'sent_at_text' => $sentAtText, 'admin_name' => $adminName, 'status' => strtolower((string)($record['status'] ?? 'sent')), ]; } foreach ($historyMap as &$teachersHistory) { foreach ($teachersHistory as &$entries) { $entries = array_slice($entries, 0, 3); } unset($entries); } unset($teachersHistory); } $summary = [ 'total_items' => $totalStatuses, 'missing_items' => $missingItemCount, 'submitted_items' => max(0, $totalStatuses - $missingItemCount), 'submission_percentage' => $totalStatuses > 0 ? (int)round((($totalStatuses - $missingItemCount) / $totalStatuses) * 100) : 100, ]; return view('administrator/teacher_submissions', [ 'rows' => $rows, 'semester' => $semester, 'schoolYear' => $schoolYear, 'notificationHistory' => $historyMap, 'summary' => $summary, ]); } public function sendTeacherSubmissionNotifications() {$notify = $this->request->getPost('notify'); if (!is_array($notify)) { return redirect()->back()->with('info', 'Select at least one teacher to notify.'); } $missingItemsPayload = $this->request->getPost('missing_items') ?? []; $targets = []; foreach ($notify as $sectionIdRaw => $teachers) { $sectionId = (int)$sectionIdRaw; if ($sectionId <= 0 || !is_array($teachers)) { continue; } foreach ($teachers as $teacherIdRaw => $value) { $teacherId = (int)$teacherIdRaw; if ($teacherId <= 0 || $value === null || $value === '') { continue; } $key = "{$sectionId}_{$teacherId}"; $targets[$key] = [ 'class_section_id' => $sectionId, 'teacher_id' => $teacherId, ]; } } if (empty($targets)) { return redirect()->back()->with('info', 'Select at least one teacher to notify.'); } $targets = array_values($targets); $teacherIds = array_values(array_unique(array_column($targets, 'teacher_id'))); $classSectionIds = array_values(array_unique(array_column($targets, 'class_section_id'))); $classSections = $this->classSectionModel ->select('class_section_id, class_section_name') ->whereIn('class_section_id', $classSectionIds) ->findAll(); $classSectionMap = []; foreach ($classSections as $section) { $classSectionMap[(int)($section['class_section_id'] ?? 0)] = $section['class_section_name'] ?? ''; } $teachers = $this->userModel ->select('id, firstname, lastname, email') ->whereIn('id', $teacherIds) ->findAll(); $teacherLookup = []; foreach ($teachers as $teacher) { $teacherLookup[(int)$teacher['id']] = $teacher; } $mailer = new EmailController(); $adminId = (int)(session()->get('user_id') ?? 0); if ($adminId <= 0) { return redirect()->to('/login'); } $adminUser = $this->userModel->find($adminId); $adminName = trim(($adminUser['firstname'] ?? '') . ' ' . ($adminUser['lastname'] ?? '')) ?: 'Administrator'; $historyModel = new TeacherSubmissionNotificationHistoryModel(); $scoreUrl = site_url('/'); $sentCount = 0; $failCount = 0; foreach ($targets as $target) { $classSectionId = (int)$target['class_section_id']; $teacherId = (int)$target['teacher_id']; $teacher = $teacherLookup[$teacherId] ?? null; $sectionName = $classSectionMap[$classSectionId] ?? "Section {$classSectionId}"; $teacherName = $teacher ? trim(($teacher['firstname'] ?? '') . ' ' . ($teacher['lastname'] ?? '')) : ''; if ($teacherName === '') { $teacherName = 'Teacher'; } $subject = "Reminder: Complete submissions for {$sectionName}"; $missingPayload = $missingItemsPayload[$classSectionId][$teacherId] ?? ''; $missingItems = $this->parseMissingItemsPayload((string)$missingPayload); if (!empty($missingItems)) { $missingText = htmlspecialchars( $this->formatMissingItemsText($missingItems), ENT_QUOTES, 'UTF-8' ); $missingNote = "

Outstanding items: {$missingText}.

"; } else { $missingNote = "

Our records show no outstanding submissions for this section, but please verify if anything still needs attention.

"; } $subject = "Reminder: Complete submissions for {$sectionName}"; $body = "

Dear {$teacherName},

" . "

Administration is gently reminding you to wrap up any remaining score submissions and/or comments for {$sectionName}.

" . $missingNote . "

Visit Teacher Score Submission to address any remaining items.

" . "

Thank you,
Al Rahma Administration

"; $email = $teacher['email'] ?? ''; $status = 'failed'; if (!empty($email) && filter_var($email, FILTER_VALIDATE_EMAIL)) { $ok = $mailer->sendEmail($email, $subject, $body, 'notifications'); $status = $ok ? 'sent' : 'failed'; } if ($status === 'sent') { $sentCount++; } else { $failCount++; } $historyModel->insert([ 'teacher_id' => $teacherId, 'class_section_id' => $classSectionId, 'admin_id' => $adminId, 'notification_category' => 'teacher_submissions', 'message' => $this->truncateNotificationMessage($body), 'status' => $status, 'school_year' => $this->schoolYear, 'semester' => $this->semester, 'sent_at' => utc_now(), ]); } $statusParts = []; if ($sentCount > 0) { $statusParts[] = $sentCount . ' reminder' . ($sentCount === 1 ? '' : 's') . ' sent'; } if ($failCount > 0) { $statusParts[] = $failCount . ' reminder' . ($failCount === 1 ? '' : 's') . ' failed'; } $message = !empty($statusParts) ? implode(' and ', $statusParts) : 'No notifications were sent.'; $flashType = $failCount === 0 ? 'success' : 'warning'; return redirect()->back()->with($flashType, $message); } private function submissionStatus(int $filled, int $expected): array { if ($expected <= 0) { return [ 'label' => 'No students', 'badge' => 'bg-secondary', 'detail' => '', 'completed' => true, ]; } $completed = $filled >= $expected; return [ 'label' => $completed ? 'Submitted' : 'Missing', 'badge' => $completed ? 'bg-success' : 'bg-danger', 'detail' => "{$filled}/{$expected}", 'completed' => $completed, ]; } private function attendanceStatus(bool $submitted): array { return [ 'label' => $submitted ? 'Submitted' : 'Missing', 'badge' => $submitted ? 'bg-success' : 'bg-danger', 'completed' => $submitted, ]; } private function buildMissingItems(array $statusMap): array { $labels = [ 'midterm_score_status' => 'midterm scores', 'midterm_comment_status' => 'midterm comments', 'participation_status' => 'participation', 'ptap_comment_status' => 'PTAP comments', 'attendance_status' => 'attendance', ]; $items = []; foreach ($statusMap as $key => $status) { $completed = $status['completed'] ?? true; if (!$completed && isset($labels[$key])) { $items[] = $labels[$key]; } } return array_values($items); } private function formatMissingItemsText(array $items): string { $items = array_values(array_filter(array_map('trim', $items), static fn($v) => $v !== '')); $count = count($items); if ($count === 0) { return ''; } if ($count === 1) { return $items[0]; } if ($count === 2) { return $items[0] . ' and ' . $items[1]; } $last = array_pop($items); return implode(', ', $items) . ' and ' . $last; } private function teacherRolePriority(string $roleKey): int { switch (strtolower($roleKey)) { case 'main': return 1; case 'ta': return 2; default: return 3; } } private function truncateNotificationMessage(string $html, int $limit = 1000): string { $text = trim(strip_tags($html)); if ($text === '') { return ''; } if (mb_strlen($text) <= $limit) { return $text; } return mb_substr($text, 0, $limit) . '…'; } private function parseMissingItemsPayload(string $payload): array { if ($payload === '') { return []; } $decoded = @json_decode(base64_decode($payload, true) ?: '', true); if (!is_array($decoded)) { return []; } $items = []; foreach ($decoded as $item) { $item = trim((string)$item); if ($item === '') { continue; } $items[] = $item; } return array_values(array_unique($items)); } public function notificationsAlerts() { if (!$this->canManageAdminNotifications()) { return redirect()->to('/login'); } $admins = $this->fetchAdminNotificationUsers(); $subjects = $this->notificationSubjectOptions(); $assignedSubjects = []; $tableReady = $this->db->tableExists('admin_notification_subjects'); if ($tableReady && !empty($admins)) { $adminIds = array_map('intval', array_column($admins, 'id')); $rows = $this->adminNotificationSubjectModel ->select('id, admin_id, subject') ->whereIn('admin_id', $adminIds) ->findAll(); foreach ($rows as $row) { $adminId = (int) ($row['admin_id'] ?? 0); $subject = (string) ($row['subject'] ?? ''); if ($adminId <= 0 || $subject === '') { continue; } if (!isset($assignedSubjects[$adminId])) { $assignedSubjects[$adminId] = []; } $assignedSubjects[$adminId][$subject] = true; } } return view('administrator/notifications_alerts', [ 'admins' => $admins, 'subjects' => $subjects, 'assignedSubjects' => $assignedSubjects, 'tableReady' => $tableReady, ]); } public function saveNotificationSubjects() { if (!$this->canManageAdminNotifications()) { return redirect()->to('/login'); } if (!$this->db->tableExists('admin_notification_subjects')) { return redirect()->to('/administrator/notifications_alerts') ->with('error', 'Notification subject storage is missing. Run migrations first.'); } $posted = $this->request->getPost('subjects'); if (!is_array($posted)) { return redirect()->to('/administrator/notifications_alerts') ->with('error', 'No selections were submitted.'); } $subjects = $this->notificationSubjectOptions(); $allowed = array_keys($subjects); $admins = $this->fetchAdminNotificationUsers(); $adminIds = array_map('intval', array_column($admins, 'id')); if (empty($adminIds)) { return redirect()->to('/administrator/notifications_alerts') ->with('info', 'No admins found to update.'); } $existing = $this->adminNotificationSubjectModel ->select('id, admin_id, subject') ->whereIn('admin_id', $adminIds) ->findAll(); $existingMap = []; foreach ($existing as $row) { $adminId = (int) ($row['admin_id'] ?? 0); $subject = (string) ($row['subject'] ?? ''); if ($adminId <= 0 || $subject === '') { continue; } if (!isset($existingMap[$adminId])) { $existingMap[$adminId] = []; } $existingMap[$adminId][$subject] = (int) ($row['id'] ?? 0); } $updates = 0; foreach ($adminIds as $adminId) { $subjectRaw = $posted[$adminId] ?? []; $selected = []; if (is_array($subjectRaw)) { foreach ($subjectRaw as $key => $value) { $candidate = is_string($key) ? $key : $value; if (!is_string($candidate)) { continue; } $candidate = trim($candidate); if ($candidate !== '') { $selected[] = $candidate; } } } $selected = array_values(array_unique(array_filter($selected, function ($value) use ($allowed) { return in_array($value, $allowed, true); }))); $current = array_keys($existingMap[$adminId] ?? []); $toDelete = array_values(array_diff($current, $selected)); $toInsert = array_values(array_diff($selected, $current)); if (!empty($toDelete)) { $this->adminNotificationSubjectModel ->where('admin_id', $adminId) ->whereIn('subject', $toDelete) ->delete(); $updates += count($toDelete); } if (!empty($toInsert)) { $batch = []; foreach ($toInsert as $subject) { $batch[] = [ 'admin_id' => $adminId, 'subject' => $subject, ]; } $this->adminNotificationSubjectModel->insertBatch($batch); $updates += count($toInsert); } } return redirect()->to('/administrator/notifications_alerts') ->with('success', $updates > 0 ? 'Notification subjects updated.' : 'No changes were made.'); } public function printNotificationRecipients() { if (!$this->canManageAdminNotifications()) { return redirect()->to('/login'); } $admins = $this->fetchAdminNotificationUsers(); $tableReady = $this->db->tableExists('admin_notification_subjects'); $assigned = []; if ($tableReady && !empty($admins)) { $adminIds = array_map('intval', array_column($admins, 'id')); $rows = $this->adminNotificationSubjectModel ->select('admin_id') ->where('subject', 'print_requests') ->whereIn('admin_id', $adminIds) ->findAll(); foreach ($rows as $row) { $adminId = (int) ($row['admin_id'] ?? 0); if ($adminId <= 0) { continue; } $assigned[$adminId] = true; } } return view('administrator/print_notification_admins', [ 'admins' => $admins, 'assigned' => $assigned, 'tableReady' => $tableReady, ]); } public function savePrintNotificationRecipients() { if (!$this->canManageAdminNotifications()) { return redirect()->to('/login'); } if (!$this->db->tableExists('admin_notification_subjects')) { return redirect()->to('/administrator/print-notifications') ->with('error', 'Notification subject storage is missing. Run migrations first.'); } $admins = $this->fetchAdminNotificationUsers(); $adminIds = array_map('intval', array_column($admins, 'id')); if (empty($adminIds)) { return redirect()->to('/administrator/print-notifications') ->with('info', 'No admins found to update.'); } $posted = (array) $this->request->getPost('notify'); $selected = []; foreach ($posted as $key => $value) { $adminId = (int) $key; if ($adminId <= 0) { continue; } if (!in_array($adminId, $adminIds, true)) { continue; } $selected[] = $adminId; } $selected = array_values(array_unique($selected)); $existingRows = $this->adminNotificationSubjectModel ->select('admin_id') ->where('subject', 'print_requests') ->whereIn('admin_id', $adminIds) ->findAll(); $current = array_values(array_unique(array_map( fn($row) => (int) ($row['admin_id'] ?? 0), $existingRows ))); $toDelete = array_values(array_diff($current, $selected)); $toInsert = array_values(array_diff($selected, $current)); if (!empty($toDelete)) { $this->adminNotificationSubjectModel ->where('subject', 'print_requests') ->whereIn('admin_id', $toDelete) ->delete(); } if (!empty($toInsert)) { $batch = []; foreach ($toInsert as $adminId) { $batch[] = [ 'admin_id' => $adminId, 'subject' => 'print_requests', ]; } $this->adminNotificationSubjectModel->insertBatch($batch); } $changes = count($toDelete) + count($toInsert); return redirect()->to('/administrator/print-notifications') ->with('success', $changes > 0 ? 'Print notification recipients updated.' : 'No changes were made.'); } private function notificationSubjectOptions(): array { return [ 'academics' => 'Academics', 'attendance' => 'Attendance', 'events' => 'Events', 'finance' => 'Finance', 'general' => 'General', 'print_requests' => 'Print Requests', ]; } private function getAdminNotificationExcludedRoles(): array { return [ 'parent', 'student', 'guest', 'teacher', 'assistant teacher', 'teacher assistant', 'teacher_assistant', 'assistant_teacher', 'ta', 'authorized_user', ]; } private function canManageAdminNotifications(): bool { $session = session(); if (! $session->get('is_logged_in')) { return false; } $role = trim((string) ($session->get('role') ?? '')); if ($role === '') { return false; } $excluded = array_map( fn ($value) => strtolower(trim((string) $value)), $this->getAdminNotificationExcludedRoles() ); return !in_array(strtolower($role), $excluded, true); } private function fetchAdminNotificationUsers(): array { $excluded = $this->getAdminNotificationExcludedRoles(); $excludedList = "'" . implode("','", $excluded) . "'"; return $this->db->table('users u') ->select('u.id, u.firstname, u.lastname, u.email') ->join('user_roles ur', 'ur.user_id = u.id', 'inner') ->join('roles r', 'r.id = ur.role_id', 'inner') ->where('r.name IS NOT NULL', null, false) ->where('ur.deleted_at', null) ->where("LOWER(r.name) NOT IN ({$excludedList})", null, false) ->groupBy('u.id, u.firstname, u.lastname, u.email') ->orderBy('u.lastname', 'ASC') ->orderBy('u.firstname', 'ASC') ->get() ->getResultArray(); } public function feeCollection() { return view('administrator/fee_collection'); } public function expenseManagement() { return view('administrator/expense_management'); } public function budgetReports() { return view('administrator/budget_reports'); } public function scholarshipInformation() { return view('administrator/scholarship_information'); } public function attendanceRecords() { return view('administrator/attendance_records'); } public function academicPerformance() { return view('administrator/academic_performance'); } public function behaviorReports() { return view('administrator/behavior_reports'); } public function healthRecords() { return view('administrator/health_records'); } public function contactInformation() { return view('administrator/contact_information'); } public function studentProfiles() { $db = db_connect(); $isPg = ($db->getPlatform() === 'Postgre'); // 'MySQLi', 'Postgre', 'SQLSRV', ... // In MySQL, avoid truncation of long lists if (!$isPg) { $db->query('SET SESSION group_concat_max_len = 8192'); } $b = $db->table('students'); if ($isPg) { // Postgres: use subqueries for aggregates to avoid GROUP BY on students.* $students = $b->select([ 'students.*', 'users.firstname AS parent_firstname', 'users.lastname AS parent_lastname', 'users.email AS parent_email', 'users.cellphone AS parent_phone', // Pick one emergency contact (first by id) "(SELECT ec.emergency_contact_name FROM emergency_contacts ec WHERE ec.parent_id = students.parent_id ORDER BY ec.id ASC LIMIT 1) AS emergency_name", "(SELECT ec.relation FROM emergency_contacts ec WHERE ec.parent_id = students.parent_id ORDER BY ec.id ASC LIMIT 1) AS emergency_relationship", "(SELECT ec.cellphone FROM emergency_contacts ec WHERE ec.parent_id = students.parent_id ORDER BY ec.id ASC LIMIT 1) AS emergency_phone", "(SELECT ec.email FROM emergency_contacts ec WHERE ec.parent_id = students.parent_id ORDER BY ec.id ASC LIMIT 1) AS emergency_email", // Aggregated lists "(SELECT STRING_AGG(DISTINCT sa.allergy, ', ' ORDER BY sa.allergy) FROM student_allergies sa WHERE sa.student_id = students.id) AS allergies", "(SELECT STRING_AGG(DISTINCT smc.condition_name, ', ' ORDER BY smc.condition_name) FROM student_medical_conditions smc WHERE smc.student_id = students.id) AS medical_conditions", ]) ->join('users', 'users.id = students.parent_id', 'left') ->orderBy('students.lastname', 'ASC') ->orderBy('students.firstname', 'ASC') ->get() ->getResultArray(); } else { // MySQL: GROUP_CONCAT + MIN() for emergency_* to satisfy ONLY_FULL_GROUP_BY $students = $b->select([ 'students.*', 'users.firstname AS parent_firstname', 'users.lastname AS parent_lastname', 'users.email AS parent_email', 'users.cellphone AS parent_phone', 'MIN(emergency_contacts.emergency_contact_name) AS emergency_name', 'MIN(emergency_contacts.relation) AS emergency_relationship', 'MIN(emergency_contacts.cellphone) AS emergency_phone', 'MIN(emergency_contacts.email) AS emergency_email', "GROUP_CONCAT(DISTINCT student_allergies.allergy ORDER BY student_allergies.allergy SEPARATOR ', ') AS allergies", "GROUP_CONCAT(DISTINCT student_medical_conditions.condition_name ORDER BY student_medical_conditions.condition_name SEPARATOR ', ') AS medical_conditions", ]) ->join('users', 'users.id = students.parent_id', 'left') ->join('emergency_contacts', 'emergency_contacts.parent_id = students.parent_id', 'left') ->join('student_allergies', 'student_allergies.student_id = students.id', 'left') ->join('student_medical_conditions', 'student_medical_conditions.student_id = students.id', 'left') ->groupBy('students.id') ->orderBy('students.lastname', 'ASC') ->orderBy('students.firstname', 'ASC') ->get() ->getResultArray(); } // === Inject class_section_name from student_class via your method and replace grade === foreach ($students as $i => $row) { $sid = (int) ($row['id'] ?? 0); if ($sid > 0) { // Fetch class_section_name for this student // Assumes your method signature: getClassSectionNameByStudentId(int $studentId): ?string $classSectionName = (string) ($this->studentClassModel->getClassSectionNameByStudentId($sid) ?? ''); // Expose it explicitly and replace original grade if present $students[$i]['class_section_name'] = $classSectionName; } else { // Keep keys consistent even if id missing $students[$i]['class_section_name'] = ''; } } // === end injection === return view('administrator/student_profiles', [ 'students' => $students, 'gradeOptions' => ['K', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'Youth'], 'genderOptions' => ['Male', 'Female', 'Other'], ]); } public function parentProfiles() { // Fetch all users with their roles in one go $allUsers = $this->userModel->findAll(); $parents = []; // Fetch roles for users in one query to avoid looping over all users foreach ($allUsers as $user) { // Get the roles of the user directly $roles = $this->userRoleModel->getRolesByUserId($user['id']); $isParent = false; // Check if $roles is iterable and contains the 'parent' role if (is_array($roles)) { foreach ($roles as $role) { if (isset($role['role_name']) && $role['role_name'] === 'parent') { $isParent = true; break; } } } if ($isParent) { // Retrieve the latest paid amount and balance for this parent $paidAmount = $this->invoiceModel->getLatestInvoicePaidAmount($user['id']) ?? 0; $balance = $this->invoiceModel->getLatestInvoiceBalance($user['id']) ?? 0; // Get students for this parent $studentsData = $this->studentModel->where('parent_id', $user['id'])->findAll(); $students = []; foreach ($studentsData as $student) { $classSectionName = $this->studentClassModel->getClassSectionNameByStudentId($student['id']) ?? 'N/A'; $students[] = [ 'name' => $student['firstname'] . ' ' . $student['lastname'], 'class_section' => $classSectionName ]; } // Add parent data to the array $parents[] = [ 'id' => $user['id'], 'school_id' => $user['school_id'], 'firstname' => $user['firstname'], 'lastname' => $user['lastname'], 'email' => $user['email'], 'cellphone' => $user['cellphone'], 'gender' => $user['gender'], 'created_at' => $user['created_at'], 'school_year' => $user['school_year'], 'paid_amount' => $paidAmount, 'balance' => $balance, 'students' => $students, ]; } } return view('administrator/parent_profile', ['parents' => $parents]); } public function manageUsers() {// Fetch all users $users = $this->userModel->findAll(); // Fetch roles for each user foreach ($users as &$user) { $userRoles = ($this->userRoleModel)->where('user_id', $user['id'])->findAll(); $user['roles'] = []; foreach ($userRoles as $userRole) { $role = $this->roleModel->find($userRole['role_id']); if ($role) { $user['roles'][] = $role['name']; } } } $data['users'] = $users; return view('administrator/manage_users', $data); } public function editUser($userId) {$user = $this->userModel->find($userId); // Fetch roles assigned to the user $userRoles = $this->userRoleModel->where('user_id', $userId)->findAll(); $assignedRoles = []; foreach ($userRoles as $userRole) { $role = $this->roleModel->find($userRole['role_id']); if ($role) { $assignedRoles[] = $role['id']; } } $roles = $this->roleModel->findAll(); $data = [ 'user' => $user, 'roles' => $roles, 'assignedRoles' => $assignedRoles, ]; return view('administrator/edit_user', $data); } public function updateUser() {$userId = $this->request->getPost('id'); $roleIds = $this->request->getPost('role_ids'); // Update user details $data = [ 'id' => $userId, 'firstname' => $this->request->getPost('firstname'), 'lastname' => $this->request->getPost('lastname'), 'email' => $this->request->getPost('email'), ]; $this->userModel->save($data); // Update user roles $this->userRoleModel->where('user_id', $userId)->delete(); foreach ($roleIds as $roleId) { $this->userRoleModel->save([ 'user_id' => $userId, 'role_id' => $roleId, ]); } return redirect()->to('/administrator/manage-users'); } public function deleteUser($userId) {// Delete user roles first $this->userRoleModel->where('user_id', $userId)->delete(); // Delete the user $this->userModel->delete($userId); return redirect()->to('/administrator/manage-users'); } public function showEnrollmentWithdrawalPage() { try { $selectedYear = trim((string)($this->request->getGet('schoolYear') ?? '')); if ($selectedYear === '') { $selectedYear = (string)$this->schoolYear; } // Distinct school years from enrollments (for selector) $yearsRows = $this->db->table('enrollments') ->distinct() ->select('school_year') ->orderBy('school_year', 'DESC') ->get()->getResultArray(); $schoolYears = array_values(array_filter(array_map(static function ($r) { return isset($r['school_year']) ? (string)$r['school_year'] : null; }, $yearsRows))); $students = $this->studentModel->getStudentsWithClassAndEnrollment(); $selectedStartYear = $this->getSchoolYearStartYear((string)$selectedYear); $removedPriorIds = []; if ($selectedStartYear !== null) { $removedRows = $this->db->table('enrollments') ->select('student_id, school_year') ->where('is_withdrawn', 1) ->get()->getResultArray(); foreach ($removedRows as $row) { $rowYear = $this->getSchoolYearStartYear((string)($row['school_year'] ?? '')); if ($rowYear !== null && $rowYear < $selectedStartYear) { $removedPriorIds[(int)($row['student_id'] ?? 0)] = true; } } } foreach ($students as &$s) { // ===== Ensure IDs needed by the modal ===== $s['student_id'] = (int)($s['id'] ?? 0); $s['removed_previous_year'] = isset($removedPriorIds[$s['student_id']]) ? 'Yes' : 'No'; // Prefer parent_id; fallback to secondparent_user_id if present if (empty($s['parent_id']) && !empty($s['secondparent_user_id'])) { $s['parent_id'] = (int)$s['secondparent_user_id']; } else { $s['parent_id'] = (int)($s['parent_id'] ?? 0); } // ===== Parent display + sort keys (keep existing behavior) ===== $pf = trim((string)($s['parent_firstname'] ?? '')); $pl = trim((string)($s['parent_lastname'] ?? '')); // Fallback: if only a single full name exists if ($pf === '' && $pl === '' && !empty($s['parent_fullname'])) { $parts = preg_split('/\s+/', trim((string)$s['parent_fullname']), 2); $pf = $parts[0] ?? ''; $pl = $parts[1] ?? ''; } $s['parent_label'] = trim($pf . ' ' . $pl); $s['parent_sort'] = trim(($pl !== '' ? $pl : $pf) . ' ' . $pf); if ($s['parent_label'] === '') { $s['parent_label'] = 'Unknown Parent'; $s['parent_sort'] = 'ZZZ Unknown Parent'; } // ===== New-student flags ===== $s['is_new'] = (int) ($s['is_new'] ?? 0); $s['new_student'] = $s['is_new'] === 1 ? 'Yes' : 'No'; // ===== Admission override ===== // Enrollment status for selected year $statusForYear = $this->enrollmentModel->getEnrollmentStatus((int)$s['student_id'], $selectedYear); if (!empty($statusForYear)) { $s['enrollment_status'] = $statusForYear; } elseif (($s['admission_status'] ?? null) === 'denied') { $s['enrollment_status'] = 'denied'; } // ===== Class section name for the selected year ===== $name = $this->studentClassModel->getClassSectionsByStudentId((int)$s['student_id'], $selectedYear); $s['class_section'] = $name ?: 'Class not Assigned'; // ===== Sortable registration date (for data-order in view) ===== $s['registration_date_order'] = !empty($s['registration_date']) ? date('Y-m-d', strtotime($s['registration_date'])) : ''; } unset($s); // break reference // ===== Sort by parent, then student (lastname, firstname) ===== usort($students, function (array $a, array $b) { $pa = $a['parent_sort'] ?? ''; $pb = $b['parent_sort'] ?? ''; if (strcasecmp($pa, $pb) === 0) { $la = $a['lastname'] ?? ''; $lb = $b['lastname'] ?? ''; $cmp = strcasecmp($la, $lb); if ($cmp !== 0) return $cmp; return strcasecmp($a['firstname'] ?? '', $b['firstname'] ?? ''); } return strcasecmp($pa, $pb); }); // ===== Provide classes for the modal select (like studentClassAssignment) ===== // Filter to current term; loosen if your table doesn’t store these fields. $classes = $this->classSectionModel ->select('id, class_section_id, class_section_name, school_year, semester') ->where('school_year', (string)$selectedYear) ->where('semester', (string)$this->semester) ->orderBy('class_section_name', 'ASC') ->findAll(); // Fallback so the dropdown never shows empty if filters are too strict if (empty($classes)) { $classes = $this->classSectionModel ->select('id, class_section_id, class_section_name') ->orderBy('class_section_name', 'ASC') ->findAll(); } return view('enroll_withdraw/enrollment_withdrawal', [ 'students' => $students, 'classes' => $classes, // <-- used by the modal