user = model(User::class); $this->teacherClass = model(TeacherClass::class); $this->config = model(Configuration::class); $this->teacher = model(Teacher::class); $this->student = model(Student::class); $this->classSection = model(ClassSection::class); $this->studentClass = model(StudentClass::class); $this->medical = model(StudentMedicalCondition::class); $this->allergy = model(StudentAllergy::class); $this->staffAttendance = model(StaffAttendance::class); $this->emailService = app(EmailService::class); $this->semester = (string) ($this->config->getConfig('semester') ?? ''); $this->schoolYear = (string) ($this->config->getConfig('school_year') ?? ''); } public function index() { $page = max(1, (int) ($this->request->getGet('page') ?? 1)); $perPage = min(100, max(1, (int) ($this->request->getGet('per_page') ?? 20))); $query = $this->user->newQuery() ->select('users.*') ->join('user_roles', 'user_roles.user_id', '=', 'users.id') ->join('roles', 'roles.id', '=', 'user_roles.role_id') ->whereIn('LOWER(roles.name)', ['teacher', 'teacher_assistant']) ->groupBy('users.id') ->orderBy('users.lastname', 'ASC'); $result = $this->paginate($query, $page, $perPage); foreach ($result['data'] as &$teacher) { unset($teacher['password'], $teacher['token']); $assignments = $this->teacherClass->newQuery() ->where('teacher_id', $teacher['id']) ->where('school_year', $this->schoolYear) ->where('semester', $this->semester) ->get() ->toArray(); $teacher['class_assignments'] = $assignments; } return $this->success($result, 'Teachers retrieved successfully'); } public function show($id = null) { $teacher = $this->user->newQuery() ->select('users.*') ->join('user_roles', 'user_roles.user_id', '=', 'users.id') ->join('roles', 'roles.id', '=', 'user_roles.role_id') ->whereIn('LOWER(roles.name)', ['teacher', 'teacher_assistant']) ->where('users.id', $id) ->first(); if (!$teacher) { return $this->respondError('Teacher not found', Response::HTTP_NOT_FOUND); } $teacherArray = $teacher->toArray(); unset($teacherArray['password'], $teacherArray['token']); $assignments = $this->teacherClass->newQuery() ->where('teacher_id', $id) ->where('school_year', $this->schoolYear) ->where('semester', $this->semester) ->get() ->toArray(); $teacherArray['class_assignments'] = $assignments; return $this->success($teacherArray, 'Teacher retrieved successfully'); } public function getClasses($id = null) { $assignments = $this->teacherClass->newQuery() ->select([ 'tc.*', 'cs.class_section_name', 'c.name as class_name', ]) ->from('teacher_class as tc') ->leftJoin('class_sections as cs', 'cs.class_section_id', '=', 'tc.class_section_id') ->leftJoin('classes as c', 'c.id', '=', 'cs.class_id') ->where('tc.teacher_id', $id) ->where('tc.school_year', $this->schoolYear) ->where('tc.semester', $this->semester) ->orderBy('cs.class_section_name', 'ASC') ->get() ->toArray(); return $this->success($assignments, 'Teacher classes retrieved successfully'); } /** * GET /api/v1/teachers/class-view * Get class view data for the logged-in teacher */ public function getClassView() { try { $userId = $this->getCurrentUserId(); if (!$userId) { return $this->respondError('User not logged in', Response::HTTP_UNAUTHORIZED); } // Fetch all assignments (as main or TA) $classes = $this->teacherClass->getClassAssignmentsByUserId($userId, $this->schoolYear); if (empty($classes)) { return $this->success([ 'has_classes' => false, 'message' => 'You do not have an assigned class yet. Please contact the administration.', ], 'No classes found'); } // Identify user and their role $user = $this->user->find($userId); if (!$user) { return $this->respondError('User not found', Response::HTTP_NOT_FOUND); } $roles = DB::table('user_roles ur') ->select('r.name') ->join('roles r', 'r.id', '=', 'ur.role_id') ->where('ur.user_id', $userId) ->get() ->map(fn($r) => strtolower($r->name)) ->toArray(); $roleName = 'teacher'; if (in_array('teacher', $roles)) { $roleName = 'teacher'; } elseif (in_array('teacher_assistant', $roles)) { $roleName = 'teacher_assistant'; } else { return $this->respondError('Access denied', Response::HTTP_FORBIDDEN); } // Collect class section IDs $classSectionIds = array_column($classes, 'class_section_id'); $studentsData = []; if (!empty($classSectionIds)) { // Fetch all students for the selected class sections $students = $this->studentClass->getStudentsByClassSectionIds($classSectionIds); if (!empty($students)) { // 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)); // Bulk fetch medical conditions & allergies grouped by student_id $conditionsByStudent = !empty($studentIds) ? $this->medical->getMedicalByStudentIds($studentIds) : []; $allergiesByStudent = !empty($studentIds) ? $this->allergy->getAllergiesByStudentIds($studentIds) : []; // Attach medical/allergy data to each student foreach ($students as $student) { $sid = (int) ($student['student_id'] ?? 0); $cid = (int) ($student['class_section_id'] ?? 0); $medList = array_column($conditionsByStudent[$sid] ?? [], 'condition_name'); $algList = array_column($allergiesByStudent[$sid] ?? [], 'allergy'); $student['medical_conditions'] = $medList; $student['allergies'] = $algList; $student['medical_conditions_text'] = implode(', ', $medList); $student['allergies_text'] = implode(', ', $algList); $studentsData[$cid][] = $student; } } } // Build assigned staff lists (all Main teachers and all TAs) per class section $assignedNames = []; if (!empty($classSectionIds)) { $rows = DB::table('teacher_class tc') ->select('tc.class_section_id', 'tc.position', 'u.firstname', 'u.lastname') ->join('users u', 'u.id', '=', 'tc.teacher_id') ->whereIn('tc.class_section_id', $classSectionIds) ->where('tc.school_year', $this->schoolYear) ->where('tc.semester', $this->semester) ->orderBy('tc.class_section_id', 'ASC') ->orderByRaw("FIELD(tc.position,'main','ta')") ->orderBy('u.firstname', 'ASC') ->get(); 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; } } } $classSectionId = $classSectionIds[0] ?? null; return $this->success([ 'teacher_name' => trim(($user['firstname'] ?? '') . ' ' . ($user['lastname'] ?? '')), 'role_name' => $roleName, 'classes' => $classes, 'students' => $studentsData[$classSectionId] ?? [], 'studentsData' => $studentsData, 'class_section_id' => $classSectionId, 'assignedNames' => $assignedNames, ], 'Class view data retrieved successfully'); } catch (\Exception $e) { Log::error('Error in TeacherController::getClassView(): ' . $e->getMessage()); return $this->respondError('An error occurred. Please try again later.', Response::HTTP_INTERNAL_SERVER_ERROR); } } /** * GET /api/v1/teachers/class-assignments * Get teacher class assignment data for administration */ public function getTeacherClassAssignments() { $forYear = trim((string) ($this->request->getGet('schoolYear') ?? $this->request->getGet('school_year') ?? '')); $payload = $this->buildTeacherClassAssignmentPayload($forYear !== '' ? $forYear : null); $payload['ok'] = true; return $this->success($payload, 'Teacher class assignments retrieved successfully'); } /** * POST /api/v1/teachers/class-assignments/assign * Assign a teacher to a class */ public function assignClassTeacher() { $data = $this->payloadData(); $result = $this->handleAssignClassTeacher($data); $statusCode = $result['ok'] ? Response::HTTP_OK : Response::HTTP_UNPROCESSABLE_ENTITY; return response()->json($result, $statusCode); } /** * DELETE /api/v1/teachers/class-assignments * Delete a teacher class assignment */ public function deleteAssignment() { $data = $this->payloadData(); $result = $this->handleDeleteAssignment($data); $statusCode = $result['ok'] ? Response::HTTP_OK : Response::HTTP_UNPROCESSABLE_ENTITY; return response()->json($result, $statusCode); } /** * GET /api/v1/teachers/absence-form * Get absence form data for the logged-in teacher */ public function getAbsenceForm() { $userId = $this->getCurrentUserId(); if (!$userId) { return $this->respondError('Please log in first.', Response::HTTP_UNAUTHORIZED); } $teacher = $this->user->find($userId); $displayName = $teacher ? trim(($teacher['firstname'] ?? '') . ' ' . ($teacher['lastname'] ?? '')) : 'Unknown'; // Fetch existing self-reported staff attendance rows for this user (current term) $existing = $this->staffAttendance ->where('user_id', $userId) ->where('semester', $this->semester) ->where('school_year', $this->schoolYear) ->orderBy('date', 'DESC') ->findAll(); return $this->success([ 'teacher_name' => $displayName, 'semester' => $this->semester, 'schoolYear' => $this->schoolYear, 'existing' => $existing, 'availableDates' => $this->allowedAbsenceDates(), ], 'Absence form data retrieved successfully'); } /** * POST /api/v1/teachers/absence * Submit absence/vacation request */ public function submitAbsence() { $userId = $this->getCurrentUserId(); if (!$userId) { return $this->respondError('Please log in first.', Response::HTTP_UNAUTHORIZED); } if ($this->semester === '' || $this->schoolYear === '') { return $this->respondError('Semester or school year not configured.', Response::HTTP_BAD_REQUEST); } $data = $this->payloadData(); $dates = (array) ($data['dates'] ?? []); $reasonType = trim((string) ($data['reason_type'] ?? '')); $reasonText = trim((string) ($data['reason'] ?? '')); if ($reasonText === '') { return $this->respondValidationError(['reason' => ['Reason is required.']]); } // Build reason string $reasonBase = ($reasonType !== '') ? strtolower($reasonType) : ''; $reason = $reasonBase !== '' ? ($reasonBase . ': ' . $reasonText) : $reasonText; // Enforce allowed Sundays list $allowedDates = $this->allowedAbsenceDates(); $allowedSet = array_fill_keys($allowedDates, true); $roleName = $this->user->getUserRole($userId) ?: null; // Validate dates and save $saved = 0; $invalid = []; $savedDates = []; $dates = array_values(array_unique(array_map('strval', $dates))); foreach ($dates as $d) { $d = trim((string) $d); if ($d === '') continue; $dt = \DateTimeImmutable::createFromFormat('Y-m-d', $d); if (!$dt || $dt->format('Y-m-d') !== $d || empty($allowedSet[$d])) { $invalid[] = $d; continue; } $ok = $this->staffAttendance->upsertOne( userId: $userId, roleName: $roleName, date: $d, semester: $this->semester, schoolYear: $this->schoolYear, status: StaffAttendance::STATUS_ABSENT, reason: $reason, editorId: $userId ); if ($ok) { $saved++; $savedDates[] = $d; } } if (!empty($invalid)) { return $this->respondError('Invalid dates: ' . implode(', ', $invalid), Response::HTTP_BAD_REQUEST); } // Send notification email to principal try { $user = $this->user->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->teacherClass->getClassAssignmentsByUserId($userId, $this->schoolYear, $this->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 = '
' . '

New TimeOff Request

' . '

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

' . '' . '' . '' . '' . '' . '' . '' . '' . '' . (in_array(strtolower((string) $roleName), ['teacher', 'teacher_assistant'], true) ? '' : '') . '' . '
Name' . esc($fullName) . '
Email' . esc($userEmail) . '
Role' . esc((string) $role) . '
Semester' . esc($this->semester) . '
School Year' . esc($this->schoolYear) . '
Reason Type' . esc($reasonType ?: '-') . '
Reason' . esc($reasonText) . '
Dates' . esc($dateList ?: '-') . '
Assigned Class(es)' . esc($assignedText) . '
Submitted At' . esc($submittedAt) . '
' . '
'; // Note: StaffTimeOffLinkService not found, skipping link generation $principalEmail = env('PRINCIPAL_EMAIL') ?: 'principal@alrahmaisgl.org'; $this->emailService->send($principalEmail, $subject, $body); } catch (\Throwable $e) { Log::error('Failed to send TimeOff email: ' . $e->getMessage()); } return $this->success([ 'saved' => $saved, 'savedDates' => $savedDates, ], $saved . ' day(s) saved as absent.'); } /** * Build teacher class assignment payload */ private function buildTeacherClassAssignmentPayload(?string $forYear = null): array { $schoolYear = $forYear ?: $this->schoolYear; $teachers = $this->teacher->getTeachersAndTAs(); // Prefer class sections for the selected year, fallback to all if none $classSections = $this->classSection ->where('school_year', $schoolYear) ->orderBy('class_section_name', 'ASC') ->findAll(); if (empty($classSections)) { $classSections = $this->classSection->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->teacherClass ->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, ]; } /** * Handle assign class teacher logic */ 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 = $this->getCurrentUserId() ?? 0; $currentDateTime = utc_now(); if ($teacherId <= 0 || $classSectionId <= 0) { return ['ok' => false, 'status' => 'error', 'message' => 'Missing required parameters for assignment.']; } if (empty($this->semester) || empty($this->schoolYear)) { return ['ok' => false, 'status' => 'error', 'message' => 'Semester or school year not configured.']; } if ($teacherRole === null || $teacherRole === '') { $teacherRole = $this->user->getUserRole($teacherId); } $isAssistant = strtolower((string) $teacherRole) === 'teacher_assistant'; $position = $isAssistant ? 'ta' : 'main'; $alreadyAssigned = $this->teacherClass->where([ 'teacher_id' => $teacherId, 'class_section_id' => $classSectionId, 'position' => $position, 'semester' => $this->semester, '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, 'semester' => $this->semester, 'school_year' => $this->schoolYear, 'created_at' => $currentDateTime, 'updated_at' => $currentDateTime, 'updated_by' => $loggedInUserId, ]; try { $this->teacherClass->insert($insertData); } catch (\Throwable $e) { Log::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, ]; } /** * Handle delete assignment logic */ 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->semester) || empty($this->schoolYear)) { return ['ok' => false, 'status' => 'error', 'message' => 'Semester or school year not configured.']; } $requestedSemester = trim((string) ($input['semester'] ?? '')); $requestedSchoolYear = trim((string) ($input['school_year'] ?? '')); $semester = $requestedSemester !== '' ? $requestedSemester : $this->semester; $schoolYear = $requestedSchoolYear !== '' ? $requestedSchoolYear : $this->schoolYear; if ($semester === '' || $schoolYear === '') { return ['ok' => false, 'status' => 'error', 'message' => 'Semester or school year not configured.']; } $assignment = $this->teacherClass ->where('teacher_id', $teacherId) ->where('class_section_id', $classSectionId) ->where('semester', $semester) ->where('school_year', $schoolYear) ->where('position', $position) ->first(); if (!$assignment) { return ['ok' => false, 'status' => 'error', 'message' => 'Assignment not found.']; } try { $this->teacherClass->delete($assignment['id']); } catch (\Throwable $e) { Log::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.', ]; } /** * Compute all allowed absence dates (Sundays) for the configured school year */ private function allowedAbsenceDates(): array { $todayStr = local_date(utc_now(), 'Y-m-d'); try { $today = new \DateTimeImmutable($todayStr); } catch (\Throwable $e) { $today = new \DateTimeImmutable(); } $sy = $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; } }