1017 lines
42 KiB
PHP
1017 lines
42 KiB
PHP
<?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 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 = '<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.');
|
||
}
|
||
}
|