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.'); } // Initialize the config model $this->configModel = new ConfigurationModel(); // Assuming ConfigModel is the model handling configurations $this->teacherClassModel = new TeacherClassModel(); $this->studentClassModel = new StudentClassModel(); $this->teacherModel = new TeacherModel(); $this->userModel = new UserModel(); $this->medicalModel = new StudentMedicalConditionModel(); $this->allergyModel = new StudentAllergyModel(); $this->staffAttendanceModel = new StaffAttendanceModel(); // Retrieve the configuration values $this->semester = $this->configModel->getConfig('semester'); $this->schoolYear = $this->configModel->getConfig('school_year'); } public function classView() { try { log_message('debug', 'TeacherController::classView() method called'); $user_id = session()->get('user_id'); if (!$user_id) { log_message('error', 'User not logged in'); return redirect()->to('user/login')->with('error', 'Please log in first'); } // Fetch all assignments (as main or TA) $classes = $this->teacherClassModel->getClassAssignmentsByUserId($user_id, $this->schoolYear); if (empty($classes)) { log_message('info', 'No classes found for teacher ID: ' . $user_id); return redirect()->to('no-classes')->with('message', 'You do not have an assigned class yet. Please contact the administration.'); } // Identify user and their role $user = $this->userModel->find($user_id); if (!$user) { log_message('error', 'User not found for ID: ' . $user_id); return redirect()->to('/error')->with('error', 'User not found'); } $roles = $this->db->table('user_roles ur') ->select('r.name') ->join('roles r', 'r.id = ur.role_id') ->where('ur.user_id', $user_id) ->get()->getResultArray(); $roleNames = array_map(fn($r) => $r['name'], $roles); if (in_array('teacher', $roleNames)) { $user['role_name'] = 'teacher'; } elseif (in_array('teacher_assistant', $roleNames)) { $user['role_name'] = 'teacher_assistant'; } else { log_message('error', 'Invalid role for user ID: ' . $user_id); return redirect()->to('/error')->with('error', 'Access denied'); } // Collect class section IDs $classSectionIds = array_map('intval', array_column($classes, 'class_section_id')); $requestedClassId = (int)($this->request->getGet('class_section_id') ?? 0); $sessionClassId = (int)(session()->get('class_section_id') ?? 0); $activeClassSectionId = null; if ($requestedClassId && in_array($requestedClassId, $classSectionIds, true)) { $activeClassSectionId = $requestedClassId; } elseif ($sessionClassId && in_array($sessionClassId, $classSectionIds, true)) { $activeClassSectionId = $sessionClassId; } elseif (!empty($classSectionIds)) { $activeClassSectionId = $classSectionIds[0]; } if ($activeClassSectionId) { session()->set('class_section_id', $activeClassSectionId); } $studentsData = []; if (!empty($classSectionIds)) { // 1) Fetch all students for the selected class sections $students = $this->studentClassModel->getStudentsByClassSectionIds($classSectionIds); if (!empty($students)) { // 2) Collect unique student IDs $studentIds = array_values(array_unique(array_map( static fn(array $row) => (int) ($row['student_id'] ?? 0), $students ))); $studentIds = array_values(array_filter($studentIds)); // 3) Bulk fetch medical conditions & allergies grouped by student_id $conditionsByStudent = !empty($studentIds) ? $this->medicalModel->getMedicalByStudentIds($studentIds) // [sid => [rows...]] : []; $allergiesByStudent = !empty($studentIds) ? $this->allergyModel->getAllergiesByStudentIds($studentIds) // [sid => [rows...]] : []; // 4) Attach only names/values (plus display strings) to each student foreach ($students as $student) { $sid = (int) ($student['student_id'] ?? 0); $cid = (int) ($student['class_section_id'] ?? 0); // Extract lists (arrays of strings) $medList = array_column($conditionsByStudent[$sid] ?? [], 'condition_name'); $algList = array_column($allergiesByStudent[$sid] ?? [], 'allergy'); // Keep arrays for logic if needed $student['medical_conditions'] = $medList; $student['allergies'] = $algList; // Also provide display-ready strings so views don’t implode() themselves $student['medical_conditions_text'] = implode(', ', $medList); $student['allergies_text'] = implode(', ', $algList); // Group under class section $studentsData[$cid][] = $student; } } } // Build assigned staff lists (all Main teachers and all TAs) per class section $assignedNames = []; if (!empty($classSectionIds)) { $rows = $this->db->table('teacher_class tc') ->select('tc.class_section_id, tc.position, u.firstname, u.lastname') ->join('users u', 'u.id = tc.teacher_id', 'inner') ->whereIn('tc.class_section_id', $classSectionIds) ->where('tc.school_year', $this->schoolYear) ->orderBy('tc.class_section_id', 'ASC') ->orderBy("FIELD(tc.position,'main','ta')", '', false) ->orderBy('u.firstname', 'ASC') ->get()->getResultArray(); foreach ($rows as $r) { $sid = (int)($r['class_section_id'] ?? 0); $pos = strtolower((string)($r['position'] ?? '')); $name = trim(($r['firstname'] ?? '') . ' ' . ($r['lastname'] ?? '')); if ($sid === 0 || $name === '') continue; if (in_array($pos, ['main', 'teacher'], true)) { $assignedNames[$sid]['mains'][] = $name; } elseif (in_array($pos, ['ta', 'assistant', 'teacher_assistant'], true)) { $assignedNames[$sid]['tas'][] = $name; } else { $assignedNames[$sid]['others'][] = $name; } } } // Log debug info log_message('debug', 'Classes: ' . print_r($classes, true)); log_message('debug', 'Students Data: ' . print_r($studentsData, true)); log_message('debug', 'Assigned Names: ' . print_r($assignedNames, true)); // First class_section_id for default selection $class_section_id = $activeClassSectionId; return view('/teacher/class_view', [ 'teacher_name' => $user['firstname'] . ' ' . $user['lastname'], 'classes' => $classes, 'students' => $studentsData[$class_section_id] ?? [], 'studentsData' => $studentsData, 'class_section_id' => $class_section_id, 'active_class_id' => $class_section_id, // Backward compat: keep taNames for view fallback if needed 'taNames' => [], 'assignedNames' => $assignedNames, ]); } catch (\Exception $e) { log_message('error', 'Error in TeacherController::classView(): ' . $e->getMessage()); return redirect()->to('/error')->with('error', 'An error occurred. Please try again later.'); } } /** * Switch the active class for the logged-in teacher. * Validates the requested class belongs to the teacher for the current term. */ public function switchClass() { $userId = (int)(session()->get('user_id') ?? 0); if (!$userId) { return redirect()->back()->with('error', 'Not authenticated.'); } $redirectTo = $this->request->getPost('redirect_to') ?? base_url('/teacher_dashboard'); $requested = (int)$this->request->getPost('class_section_id'); $assignments = $this->teacherClassModel->getClassAssignmentsByUserId( $userId, (string)$this->schoolYear, (string)$this->semester ); $allowedIds = array_map(static fn($a) => (int)($a['class_section_id'] ?? 0), $assignments); $allowedIds = array_values(array_filter(array_unique($allowedIds))); if ($requested && in_array($requested, $allowedIds, true)) { session()->set('class_section_id', $requested); } else { return redirect()->to($redirectTo)->with('error', 'You are not assigned to that class.'); } return redirect()->to($redirectTo); } public function noClasses() { return view('/teacher/no_classes'); } public function teacherClassAssignment() { $selectedYear = trim((string)($this->request->getGet('schoolYear') ?? $this->request->getGet('school_year') ?? '')); if ($selectedYear === '') { $selectedYear = (string)($this->schoolYear ?? 'Not Set'); } // Build distinct school years from classSection (fallback to configured year) try { $yearsRows = $this->db->table('classSection') ->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))); if (empty($schoolYears) && !empty($this->schoolYear)) { $schoolYears = [(string)$this->schoolYear]; } } catch (\Throwable $e) { $schoolYears = !empty($this->schoolYear) ? [(string)$this->schoolYear] : []; } return view('administrator/teacher_class_assignment', [ 'schoolYear' => (string)$selectedYear, 'schoolYears' => $schoolYears, 'currentYear' => (string)($this->schoolYear ?? ''), 'isCurrentYear' => ((string)$selectedYear === (string)($this->schoolYear ?? '')), 'missingYear' => empty($this->schoolYear), 'apiListUrl' => site_url('api/administrator/teacher-class/assignments'), 'apiAssignUrl' => site_url('api/administrator/teacher-class/assign'), 'apiDeleteUrl' => site_url('api/administrator/teacher-class/delete'), 'csrfTokenName' => csrf_token(), 'csrfTokenValue' => csrf_hash(), ]); } private function buildTeacherClassAssignmentPayload(?string $forYear = null): array { $classSectionModel = new ClassSectionModel(); $schoolYear = $forYear ?: ((string)($this->schoolYear ?? 'Not Set')); $teachers = $this->teacherModel->getTeachersAndTAs(); // Prefer class sections for the selected year if the column exists, otherwise fallback to all. if ($this->db->fieldExists('school_year', 'classSection')) { $classSections = $classSectionModel ->where('school_year', $schoolYear) ->orderBy('class_section_name', 'ASC') ->findAll(); if (empty($classSections)) { $classSections = $classSectionModel->orderBy('class_section_name', 'ASC')->findAll(); } } else { $classSections = $classSectionModel->orderBy('class_section_name', 'ASC')->findAll(); } $classNames = []; foreach ($classSections as $section) { $sectionId = (int)($section['class_section_id'] ?? $section['id'] ?? 0); if ($sectionId <= 0) { continue; } $classNames[$sectionId] = $section['class_section_name'] ?? ($section['name'] ?? 'N/A'); } $assignments = $this->teacherClassModel ->where('school_year', $schoolYear) ->findAll(); $assignmentMap = []; foreach ($assignments as $assign) { $teacherId = (int)($assign['teacher_id'] ?? 0); $sectionId = (int)($assign['class_section_id'] ?? 0); if ($teacherId <= 0 || $sectionId <= 0) { continue; } $role = strtolower((string)($assign['position'] ?? '')); if (!in_array($role, ['main', 'ta'], true)) { continue; } if (!isset($assignmentMap[$teacherId])) { $assignmentMap[$teacherId] = ['main' => [], 'ta' => []]; } $assignmentMap[$teacherId][$role][] = [ 'section_id' => $sectionId, 'class_section_name' => $classNames[$sectionId] ?? 'N/A', 'role' => $role, 'semester' => (string)($assign['semester'] ?? $this->semester ?? ''), 'school_year' => (string)($assign['school_year'] ?? $schoolYear), ]; } $teacherData = []; foreach ($teachers as $teacher) { $tid = (int)($teacher['id'] ?? 0); if ($tid <= 0) { continue; } $assigned = $assignmentMap[$tid] ?? ['main' => [], 'ta' => []]; $firstName = $teacher['firstname'] ?? ''; $lastName = $teacher['lastname'] ?? ''; $name = trim($firstName . ' ' . $lastName); if ($name === '') { $name = 'User#' . $tid; } $teacherData[] = [ 'teacher_id' => $tid, 'firstname' => $firstName, 'lastname' => $lastName, 'name' => $name, 'email' => $teacher['email'] ?? '', 'cellphone' => $teacher['cellphone'] ?? '', 'role' => $teacher['role'] ?? '', 'school_year' => $this->schoolYear, 'main_assignments' => $assigned['main'], 'ta_assignments' => $assigned['ta'], ]; } $classesPayload = []; foreach ($classSections as $section) { $sectionId = (int)($section['class_section_id'] ?? $section['id'] ?? 0); if ($sectionId <= 0) { continue; } $classesPayload[] = [ 'class_section_id' => $sectionId, 'class_section_name' => $section['class_section_name'] ?? ($section['name'] ?? 'N/A'), ]; } return [ 'school_year' => $schoolYear, 'teachers' => $teacherData, 'classes' => $classesPayload, ]; } private function respondAssignmentJson(array $payload, int $statusCode = 200) { $payload['csrf_token'] = csrf_token(); $payload['csrf_hash'] = csrf_hash(); return $this->response->setStatusCode($statusCode)->setJSON($payload); } public function teacherClassAssignmentsApi() { $forYear = trim((string)($this->request->getGet('schoolYear') ?? $this->request->getGet('school_year') ?? '')); $payload = $this->buildTeacherClassAssignmentPayload($forYear !== '' ? $forYear : null); $payload['ok'] = true; return $this->respondAssignmentJson($payload); } public function refreshTeacherAssignments() { $teacherAssignments = $this->teacherClassModel->findAll(); // Return the data as JSON return $this->response->setJSON([ 'status' => 'success', 'data' => $teacherAssignments ]); } public function messageTeacher() { return view('/teacher/teacher_message'); } public function assignmentTeacher() { return view('/teacher/teacher_assignment'); } private function handleAssignClassTeacher(array $input): array { $teacherId = (int)($input['teacher_id'] ?? 0); $classSectionId = (int)($input['class_section_id'] ?? 0); $teacherRole = $input['teacher_role'] ?? null; $loggedInUserId = (int)(session()->get('user_id') ?? 0); $currentDateTime = utc_now(); if ($teacherId <= 0 || $classSectionId <= 0) { return ['ok' => false, 'status' => 'error', 'message' => 'Missing required parameters for assignment.']; } if (empty($this->schoolYear)) { return ['ok' => false, 'status' => 'error', 'message' => 'School year not configured.']; } if ($teacherRole === null || $teacherRole === '') { $teacherRole = $this->userModel->getUserRole($teacherId); } $isAssistant = strtolower((string)$teacherRole) === 'teacher_assistant'; $position = $isAssistant ? 'ta' : 'main'; $alreadyAssigned = $this->teacherClassModel->where([ 'teacher_id' => $teacherId, 'class_section_id' => $classSectionId, 'position' => $position, 'school_year' => $this->schoolYear, ])->first(); if ($alreadyAssigned) { return ['ok' => false, 'status' => 'info', 'message' => 'This teacher is already assigned to this class with the same role.']; } $insertData = [ 'teacher_id' => $teacherId, 'class_section_id' => $classSectionId, 'position' => $position, 'school_year' => $this->schoolYear, 'created_at' => $currentDateTime, 'updated_at' => $currentDateTime, 'updated_by' => $loggedInUserId, ]; try { $this->teacherClassModel->insert($insertData); } catch (\Throwable $e) { log_message('error', 'assignClassTeacher failed: ' . $e->getMessage()); return ['ok' => false, 'status' => 'error', 'message' => 'Unable to assign class. Please try again.']; } return [ 'ok' => true, 'status' => 'success', 'message' => 'Class assigned to ' . ($isAssistant ? 'Teacher Assistant' : 'Main Teacher') . ' successfully.', 'position'=> $position, ]; } public function assignClassTeacher() { $result = $this->handleAssignClassTeacher([ 'teacher_id' => $this->request->getPost('teacher_id'), 'class_section_id' => $this->request->getPost('class_section_id'), 'teacher_role' => $this->request->getPost('teacher_role'), ]); if ($this->request->wantsJSON() || $this->request->isAJAX()) { $statusCode = $result['ok'] ? 200 : 422; return $this->respondAssignmentJson($result, $statusCode); } session()->setFlashdata($result['status'], $result['message']); return redirect()->to('administrator/teacher_class_assignment'); } public function assignClassTeacherApi() { $payload = $this->request->getJSON(true) ?? []; $result = $this->handleAssignClassTeacher($payload); $statusCode = $result['ok'] ? 200 : 422; return $this->respondAssignmentJson($result, $statusCode); } // Functions for teacher assignments private function createNewAssignment($teacherClassModel, $teacherId, $classSectionId, $loggedInUserId, $currentDateTime) { // Get the teacher's role to determine position $teacherRole = $this->userModel->getUserRole($teacherId); $position = (strtolower($teacherRole) === 'teacher_assistant') ? 'ta' : 'main'; // Prevent assigning TA without a main teacher already present if ($position === 'ta') { $mainExists = $teacherClassModel->where([ 'class_section_id' => $classSectionId, 'position' => 'main', 'school_year' => $this->schoolYear, ])->first(); if (!$mainExists) { session()->setFlashdata('error', 'Cannot assign a Teacher Assistant without a Main Teacher assigned first.'); return $this->teacherClassAssignment(); } } // Check for position conflict before inserting $positionConflict = $teacherClassModel->where([ 'class_section_id' => $classSectionId, 'position' => $position, 'school_year' => $this->schoolYear, ])->first(); if ($positionConflict) { session()->setFlashdata('error', ucfirst($position) . ' is already assigned to this class.'); return $this->teacherClassAssignment(); } // Insert new record $teacherClassModel->insert([ 'teacher_id' => $teacherId, 'class_section_id' => $classSectionId, 'position' => $position, 'school_year' => $this->schoolYear, 'updated_by' => $loggedInUserId, 'created_at' => $currentDateTime, 'updated_at' => $currentDateTime ]); session()->setFlashdata('success', 'Teacher assigned successfully.'); return $this->teacherClassAssignment(); } private function updateTeacherAssignment($existingClass, $teacherId, $teacherClassModel, $loggedInUserId, $currentDateTime) { $teacherRole = $this->userModel->getUserRole($teacherId); $position = (strtolower($teacherRole) === 'teacher_assistant') ? 'ta' : 'main'; // Check if same class_section_id already has a main/ta $positionConflict = $teacherClassModel->where([ 'class_section_id' => $existingClass['class_section_id'], 'position' => $position, 'school_year' => $this->schoolYear, ])->first(); if ($positionConflict && $positionConflict['teacher_id'] !== $teacherId) { session()->setFlashdata('error', ucfirst($position) . ' already assigned to this class.'); return $this->teacherClassAssignment(); } $teacherClassModel->update($existingClass['id'], [ 'teacher_id' => $teacherId, 'position' => $position, 'updated_by' => $loggedInUserId, 'updated_at' => $currentDateTime ]); session()->setFlashdata('success', ucfirst($position) . ' reassigned successfully.'); return $this->teacherClassAssignment(); } private function clearupdateTeacherAssignment($existingAssignment, $teacherId, $teacherClassModel, $loggedInUserId, $currentDateTime) { // Step 1: Delete all TAs for this class_section + school_year $teacherClassModel->where([ 'class_section_id' => $existingAssignment['class_section_id'], 'position' => 'ta', 'school_year' => $this->schoolYear, ])->delete(); // Step 2: Update the main teacher assignment $teacherClassModel->update($existingAssignment['id'], [ 'teacher_id' => $teacherId, 'position' => 'main', 'updated_by' => $loggedInUserId, 'updated_at' => $currentDateTime ]); session()->setFlashdata('success', 'Main teacher reassigned and assistants cleared.'); return $this->teacherClassAssignment(); } private function updateTeacherAssignmentForSameClass($existingClass, $teacherClassModel, $loggedInUserId, $currentDateTime) { // No changes to assistants needed — schema uses separate rows per person $teacherClassModel->update($existingClass['id'], [ 'updated_by' => $loggedInUserId, 'updated_at' => $currentDateTime ]); session()->setFlashdata('info', 'This class is already assigned to the teacher.'); return $this->teacherClassAssignment(); } private function assignNewAssistant($existingClass, $teacherClassModel, $teacherId, $classSectionId, $loggedInUserId, $currentDateTime) { // Check if TA already assigned to this class for the current school year $alreadyAssigned = $teacherClassModel->where([ 'teacher_id' => $teacherId, 'class_section_id' => $classSectionId, 'school_year' => $this->schoolYear, 'position' => 'ta' ])->first(); if ($alreadyAssigned) { session()->setFlashdata('info', 'This assistant is already assigned to the class.'); return $this->teacherClassAssignment(); } // Insert a new row for the assistant $teacherClassModel->insert([ 'teacher_id' => $teacherId, 'class_section_id' => $classSectionId, 'school_year' => $this->schoolYear, 'position' => 'ta', 'created_at' => $currentDateTime, 'updated_at' => $currentDateTime, 'updated_by' => $loggedInUserId ]); session()->setFlashdata('success', 'Assistant assigned successfully.'); return $this->teacherClassAssignment(); } private function updateAssistantAssignment($existingAssignment, $classSectionId, $teacherClassModel, $loggedInUserId, $currentDateTime) { $teacherClassModel->update($existingAssignment['id'], [ 'class_section_id' => $classSectionId, 'updated_by' => $loggedInUserId, 'updated_at' => $currentDateTime ]); session()->setFlashdata('success', 'Assistant reassigned successfully.'); return $this->teacherClassAssignment(); } private function updateAssistantForSameClass($existingAssignment, $existingClass, $teacherId, $teacherClassModel, $loggedInUserId, $currentDateTime) { // If the assignment already exists and class is the same, flash info if ($existingAssignment['class_section_id'] == $existingClass['class_section_id']) { session()->setFlashdata('info', 'This assistant is already assigned to the selected class.'); return $this->teacherClassAssignment(); } // Otherwise, update to correct class_section $teacherClassModel->update($existingAssignment['id'], [ 'class_section_id' => $existingClass['class_section_id'], 'updated_by' => $loggedInUserId, 'updated_at' => $currentDateTime ]); session()->setFlashdata('success', 'Assistant moved to the correct class.'); return $this->teacherClassAssignment(); } private function handleDeleteAssignment(array $input): array { $teacherId = (int)($input['teacher_id'] ?? 0); $classSectionId = (int)($input['class_section_id'] ?? 0); $position = strtolower((string)($input['position'] ?? '')); if ($teacherId <= 0 || $classSectionId <= 0 || !in_array($position, ['main', 'ta'], true)) { return ['ok' => false, 'status' => 'error', 'message' => 'Missing or invalid data for deletion.']; } if (empty($this->schoolYear)) { return ['ok' => false, 'status' => 'error', 'message' => 'School year not configured.']; } $requestedSchoolYear = trim((string)($input['school_year'] ?? '')); $schoolYear = $requestedSchoolYear !== '' ? $requestedSchoolYear : $this->schoolYear; if ($schoolYear === '') { return ['ok' => false, 'status' => 'error', 'message' => 'School year not configured.']; } $assignment = $this->teacherClassModel ->where('teacher_id', $teacherId) ->where('class_section_id', $classSectionId) ->where('school_year', $schoolYear) ->where('position', $position) ->first(); if (!$assignment) { return ['ok' => false, 'status' => 'error', 'message' => 'Assignment not found.']; } try { $this->teacherClassModel->delete($assignment['id']); } catch (\Throwable $e) { log_message('error', 'deleteAssignment failed: ' . $e->getMessage()); return ['ok' => false, 'status' => 'error', 'message' => 'Unable to remove assignment. Please try again.']; } return [ 'ok' => true, 'status' => 'success', 'message' => ucfirst($position) . ' assignment removed successfully.', ]; } public function deleteAssignment() { $result = $this->handleDeleteAssignment([ 'teacher_id' => $this->request->getPost('teacher_id'), 'class_section_id' => $this->request->getPost('class_section_id'), 'position' => $this->request->getPost('position'), ]); if ($this->request->wantsJSON() || $this->request->isAJAX()) { $statusCode = $result['ok'] ? 200 : 422; return $this->respondAssignmentJson($result, $statusCode); } session()->setFlashdata($result['status'], $result['message']); return redirect()->to('administrator/teacher_class_assignment'); } public function deleteAssignmentApi() { $payload = $this->request->getJSON(true) ?? []; $result = $this->handleDeleteAssignment($payload); $statusCode = $result['ok'] ? 200 : 422; return $this->respondAssignmentJson($result, $statusCode); } /** * Compute all allowed absence dates (Sundays) for the configured school year * limited to future dates from today, within Sep (start year) to May (end 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 { // Fallback based on current date $cy = (int)date('Y'); $cm = (int)date('n'); if ($cm >= 9) { // Sep-Dec $startYear = $cy; $endYear = $cy + 1; } else { // Jan-Aug $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 []; } // Clamp start to today if in range if ($start < $today) { $start = $today; } // Iterate and collect Sundays in range [start..end] $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; } /** * Show self-service absence/vacation page for the logged-in teacher/TA. * Lists only the current user and allows selecting days to mark as absent with a reason. */ public function absenceForm() { $userId = (int)(session()->get('user_id') ?? 0); if ($userId <= 0) { return redirect()->to('login')->with('error', 'Please log in first.'); } $teacher = $this->userModel->find($userId); $displayName = $teacher ? trim(($teacher['firstname'] ?? '') . ' ' . ($teacher['lastname'] ?? '')) : 'Unknown'; $semester = (string)($this->semester ?? ''); $schoolYear = (string)($this->schoolYear ?? ''); // Fetch existing self-reported staff attendance rows for this user (current term) $existing = $this->staffAttendanceModel ->where('user_id', $userId) ->where('semester', $semester) ->where('school_year', $schoolYear) ->orderBy('date', 'DESC') ->findAll(); return view('/teacher/absence_vacation', [ 'teacher_name' => $displayName, 'semester' => $semester, 'schoolYear' => $schoolYear, 'existing' => $existing, 'availableDates' => $this->allowedAbsenceDates(), ]); } /** * Save absence/vacation selections for the logged-in teacher. * Writes to staff_attendance as status=absent with a provided reason. */ public function submitAbsence() { $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('/teacher/absence')->with('status', 'error')->with('message', 'Semester or school year not configured.'); } $semesterResolver = new SemesterRangeService($this->configModel); // Gather inputs $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('/teacher/absence') ->with('status', 'error') ->with('message', 'Reason is required.'); } // Build reason string (combine category + text when type is present) $reasonBase = ($reasonType !== '') ? strtolower($reasonType) : ''; $reason = $reasonBase !== '' ? ($reasonBase . ': ' . $reasonText) : $reasonText; // Enforce allowed Sundays list (future Sundays in Sep..May for the term) $allowedDates = $this->allowedAbsenceDates(); $allowedSet = array_fill_keys($allowedDates, true); // Find one role for logging (optional field) $roleName = $this->userModel->getUserRole($userId) ?: null; // Validate dates and save $saved = 0; $invalid = []; $savedDates = []; // de-duplicate selected dates $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('/teacher/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'] ?? '')) ?: 'Unknown'; $userEmail = $user['email'] ?? ''; $role = $roleName ?: 'staff'; $dateList = !empty($savedDates) ? implode(', ', $savedDates) : implode(', ', $dates); // If teacher/TA, include assigned class(es) for current term $assignedText = '-'; if (in_array(strtolower((string)$roleName), ['teacher', 'teacher_assistant'], true)) { try { $assignments = $this->teacherClassModel->getClassAssignmentsByUserId($userId, $schoolYear, $semester); if (!empty($assignments)) { $parts = []; foreach ($assignments as $as) { $name = (string)($as['class_section_name'] ?? ''); $r = (string)($as['teacher_role'] ?? ''); if ($name !== '') { $parts[] = $r ? ($name . ' (' . $r . ')') : $name; } } if (!empty($parts)) { $assignedText = implode(', ', $parts); } else { $assignedText = 'No assigned class'; } } else { $assignedText = 'No assigned class'; } } catch (\Throwable $e) { $assignedText = 'N/A'; } } $subject = sprintf('TimeOff Request: %s (%s) — %s', $fullName, ucfirst((string)$role), $dateList ?: 'No dates'); $submittedAt = utc_now(); $body = '
A staff time-off request was submitted from the teacher portal.
' . '| Name | ' . esc($fullName) . ' |
| ' . esc($userEmail) . ' | |
| Role | ' . esc((string)$role) . ' |
| Semester | ' . esc($semester) . ' |
| School Year | ' . esc($schoolYear) . ' |
| Reason Type | ' . esc($reasonType ?: '-') . ' |
| Reason | ' . esc($reasonText) . ' |
| Dates | ' . esc($dateList ?: '-') . ' |
| Assigned Class(es) | ' . esc($assignedText) . ' |
| Submitted At | ' . esc($submittedAt) . ' |
Click Send confirmation email to ' . esc($fullName) . ' so the staff member is notified automatically. ' . 'This link expires in 14 days.
'; // Use configured EmailService with notifications sender profile $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: ' . $e->getMessage()); } return redirect()->to('/teacher/absence') ->with('status', 'success') ->with('message', $saved . ' day(s) saved as absent.'); } }