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')
->join('students', 'students.id = student_class.student_id', 'inner')
->where('student_class.school_year', $this->schoolYear)
->where('student_class.class_section_id IS NOT NULL', null, false)
->where('students.is_active', 1)
->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