Files
alrahma_sunday_school_api/app/old/TeacherController.php
T
2026-03-09 16:03:16 -04:00

1013 lines
42 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace App\Controllers\View;
use App\Models\UserModel;
use App\Models\TeacherModel;
use App\Models\StudentModel;
use App\Models\ClassSectionModel;
use App\Models\TeacherClassModel;
use App\Models\ConfigurationModel;
use App\Controllers\BaseController;
use App\Models\StudentClassModel;
use App\Models\StudentMedicalConditionModel;
use App\Models\StudentAllergyModel;
use App\Models\StaffAttendanceModel;
use App\Libraries\StaffTimeOffLinkService;
use App\Services\SemesterRangeService;
use CodeIgniter\Controller;
class TeacherController extends BaseController
{
protected $db;
protected $configModel;
protected $semester;
protected $schoolYear;
protected $teacherClassModel;
protected $studentClassModel;
protected $teacherModel;
protected $userModel;
protected $medicalModel;
protected $allergyModel;
protected $staffAttendanceModel;
public function __construct()
{
// 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.');
}
// 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 dont 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, fallback to all if none
$classSections = $classSectionModel
->where('school_year', $schoolYear)
->orderBy('class_section_name', 'ASC')
->findAll();
if (empty($classSections)) {
$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 = '<div style="font-family:Arial,Helvetica,sans-serif;font-size:14px;line-height:1.5;">'
. '<h3 style="margin:0 0 8px;">New TimeOff Request</h3>'
. '<p style="margin:0 0 12px;">A staff time-off request was submitted from the teacher portal.</p>'
. '<table cellpadding="6" cellspacing="0" style="border-collapse:collapse;">'
. '<tr><td><strong>Name</strong></td><td>' . esc($fullName) . '</td></tr>'
. '<tr><td><strong>Email</strong></td><td>' . esc($userEmail) . '</td></tr>'
. '<tr><td><strong>Role</strong></td><td>' . esc((string)$role) . '</td></tr>'
. '<tr><td><strong>Semester</strong></td><td>' . esc($semester) . '</td></tr>'
. '<tr><td><strong>School Year</strong></td><td>' . esc($schoolYear) . '</td></tr>'
. '<tr><td><strong>Reason Type</strong></td><td>' . esc($reasonType ?: '-') . '</td></tr>'
. '<tr><td><strong>Reason</strong></td><td>' . esc($reasonText) . '</td></tr>'
. '<tr><td><strong>Dates</strong></td><td>' . esc($dateList ?: '-') . '</td></tr>'
. (in_array(strtolower((string)$roleName), ['teacher','teacher_assistant'], true)
? '<tr><td><strong>Assigned Class(es)</strong></td><td>' . esc($assignedText) . '</td></tr>'
: '')
. '<tr><td><strong>Submitted At</strong></td><td>' . esc($submittedAt) . '</td></tr>'
. '</table>'
. '</div>';
$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' => 'teacher portal',
]);
$notifyUrl = base_url('timeoff/notify/' . rawurlencode($token));
$body .= '<p style="margin-top:16px;">Click <a href="' . esc($notifyUrl) . '">Send confirmation email to '
. esc($fullName) . '</a> so the staff member is notified automatically. '
. 'This link expires in 14 days.</p>';
// 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.');
}
}