user = model(User::class); $this->student = model(Student::class); $this->config = model(Configuration::class); $this->loginActivity = model(LoginActivity::class); $this->enrollment = model(Enrollment::class); $this->refund = model(Refund::class); $this->invoice = model(Invoice::class); $this->classSection = model(ClassSection::class); $this->userRole = model(UserRole::class); $this->studentClass = model(StudentClass::class); $this->staffAttendance= model(StaffAttendance::class); $this->db = \Config\Database::connect(); $this->schoolYear = $this->config->getConfig('school_year'); $this->semester = $this->config->getConfig('semester'); } /** * Get dashboard metrics * GET /api/v1/administrator/dashboard */ public function dashboard() { try { $recentActivities = $this->loginActivity->getLastActivities(4); if (!is_array($recentActivities)) { $recentActivities = []; } $totalAdmins = (int) ($this->user->countAdminsBySchoolYear($this->schoolYear) ?? 0); $teachers = $this->user->getUsersByRoleAndSchoolYear('teacher', $this->schoolYear); $totalTeachers = count(array_unique(array_column($teachers, 'id'))); $teacherAssistants = $this->user->getUsersByRoleAndSchoolYear('teacher_assistant', $this->schoolYear); $totalTeacherAssistants = count(array_unique(array_column($teacherAssistants, 'id'))); $parents = $this->user->getUsersByRoleAndSchoolYear('parent', $this->schoolYear); $totalParents = count(array_unique(array_column($parents, 'id'))); $totalStudents = (int) ( $this->db->table('student_class') ->select('COUNT(DISTINCT student_class.student_id) AS cnt') ->where('student_class.school_year', $this->schoolYear) ->where('student_class.class_section_id IS NOT NULL', null, false) ->get() ->getRow('cnt') ?? 0 ); // Align shape with View\AdministratorController::buildDashboardMetrics() $data = [ 'counts' => [ 'students' => $totalStudents, 'teachers' => $totalTeachers, 'teacherAssistants' => $totalTeacherAssistants, 'admins' => $totalAdmins, 'parents' => $totalParents, ], 'recentActivities' => array_map(static function ($activity) { if (!is_array($activity)) { return []; } return [ 'login_time' => $activity['login_time'] ?? null, 'email' => $activity['email'] ?? null, ]; }, $recentActivities), 'meta' => [ 'schoolYear' => $this->schoolYear, 'semester' => $this->semester, ], ]; return $this->success($data, 'Dashboard metrics retrieved successfully'); } catch (\Exception $e) { log_message('error', 'Dashboard metrics error: ' . $e->getMessage()); return $this->error('Failed to retrieve dashboard metrics', 500); } } /** * Compute allowed absence dates (future Sundays within Sep..May for configured school year) */ private function allowedAbsenceDates(): array { $todayStr = local_date(utc_now(), 'Y-m-d'); try { $today = new \DateTimeImmutable($todayStr); } catch (\Throwable $e) { $today = new \DateTimeImmutable(); } $sy = (string)($this->schoolYear ?? ''); $startYear = null; $endYear = null; if (preg_match('/^(\d{4})\D+(\d{4})$/', $sy, $m)) { $startYear = (int)$m[1]; $endYear = (int)$m[2]; } else { $cy = (int)date('Y'); $cm = (int)date('n'); if ($cm >= 9) { // Sep-Dec $startYear = $cy; $endYear = $cy + 1; } else { $startYear = $cy - 1; $endYear = $cy; } } try { $start = new \DateTimeImmutable(sprintf('%04d-09-01', $startYear)); $end = new \DateTimeImmutable(sprintf('%04d-05-31', $endYear)); } catch (\Throwable $e) { return []; } if ($start < $today) { $start = $today; } $dates = []; for ($cursor = $start; $cursor <= $end; $cursor = $cursor->modify('+1 day')) { if ((int)$cursor->format('w') === 0) { // Sunday $dates[] = $cursor->format('Y-m-d'); } } return $dates; } /** * GET /api/v1/administrator/absence * Return current admin absence context and allowed dates */ public function absenceInfo() { $userId = $this->getCurrentUserId(); if (!$userId) { return $this->error('Unauthorized', 401); } $admin = $this->user->find($userId); $displayName = $admin ? trim(($admin['firstname'] ?? '') . ' ' . ($admin['lastname'] ?? '')) : 'Administrator'; $semester = (string)($this->semester ?? ''); $schoolYear = (string)($this->schoolYear ?? ''); $existing = $this->staffAttendance ->where('user_id', $userId) ->where('semester', $semester) ->where('school_year', $schoolYear) ->orderBy('date', 'DESC') ->findAll(); return $this->success([ 'admin_name' => $displayName, 'semester' => $semester, 'school_year' => $schoolYear, 'existing' => $existing, 'available_dates'=> $this->allowedAbsenceDates(), ], 'Absence info retrieved'); } /** * POST /api/v1/administrator/absence * Submit absence entries for current admin * Payload: { dates: ["YYYY-MM-DD",...], reason_type?: string, reason: string } */ public function submitAbsence() { $userId = $this->getCurrentUserId(); if (!$userId) { return $this->error('Unauthorized', 401); } $payload = $this->request->getJSON(true) ?? []; $dates = (array)($payload['dates'] ?? []); $reasonType = trim((string)($payload['reason_type'] ?? '')); $reasonText = trim((string)($payload['reason'] ?? '')); $semester = (string)($this->semester ?? ''); $schoolYear = (string)($this->schoolYear ?? ''); if ($semester === '' || $schoolYear === '') { return $this->error('Semester or school year not configured', 500); } if ($reasonText === '') { return $this->error('Reason is required', 422); } $reasonBase = ($reasonType !== '') ? strtolower($reasonType) : ''; $reason = $reasonBase !== '' ? ($reasonBase . ': ' . $reasonText) : $reasonText; $allowedDates = $this->allowedAbsenceDates(); $allowedSet = array_fill_keys($allowedDates, true); $roleName = $this->user->getUserRole($userId) ?: null; $saved = 0; $invalid = []; $savedDates = []; $dates = array_values(array_unique(array_map('strval', $dates))); foreach ($dates as $d) { $d = trim((string)$d); if ($d === '') continue; $dt = date_create_from_format('Y-m-d', $d); if (!$dt || $dt->format('Y-m-d') !== $d || empty($allowedSet[$d])) { $invalid[] = $d; continue; } $ok = $this->staffAttendance->upsertOne( userId: $userId, roleName: $roleName, date: $d, semester: $semester, schoolYear: $schoolYear, status: StaffAttendance::STATUS_ABSENT, reason: $reason, editorId: $userId ); if ($ok) { $saved++; $savedDates[] = $d; } } // Best-effort principal email try { $user = $this->user->find($userId) ?: []; $fullName = trim(($user['firstname'] ?? '') . ' ' . ($user['lastname'] ?? '')) ?: 'Administrator'; $userEmail = $user['email'] ?? ''; $role = $roleName ?: 'admin'; $dateList = !empty($savedDates) ? implode(', ', $savedDates) : implode(', ', $dates); $subject = sprintf('TimeOff Request: %s (%s) — %s', $fullName, ucfirst((string)$role), $dateList ?: 'No dates'); $body = '
' . '

New TimeOff Request

' . '

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

' . '' . '' . '' . '' . '' . '' . '' . '' . '' . '' . '
Name' . esc($fullName) . '
Email' . esc($userEmail) . '
Role' . esc((string)$role) . '
Semester' . esc($semester) . '
School Year' . esc($schoolYear) . '
Reason Type' . esc($reasonType ?: '-') . '
Reason' . esc($reasonText) . '
Dates' . esc($dateList ?: '-') . '
Submitted At' . esc(utc_now()) . '
' . '
'; $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 (api admin): ' . $e->getMessage()); } return $this->success([ 'saved' => $saved, 'invalid' => $invalid, 'saved_dates' => $savedDates, ], $saved . ' day(s) saved as absent.'); } /** * Search users, students, parents, staff, emergency contacts * GET /api/v1/administrator/search */ public function search() { $q = trim((string)($this->request->getGet('q') ?? '')); if ($q === '') { return $this->error('Search query is required', 400); } try { $db = $this->db; // 1) Tokenize $rawTokens = preg_split('/[,\s]+/u', $q, -1, PREG_SPLIT_NO_EMPTY) ?: []; $tokens = array_values(array_filter(array_map('trim', $rawTokens))); // 2) Phone variants per token $phoneMap = []; foreach ($tokens as $t) { $digits = preg_replace('/\D+/', '', $t); if ($digits === '') continue; $v = []; if (strlen($digits) >= 7) { $v[] = $digits; if (strlen($digits) === 10) { $v[] = sprintf('(%s)-%s-%s', substr($digits, 0, 3), substr($digits, 3, 3), substr($digits, 6)); $v[] = sprintf('%s-%s-%s', substr($digits, 0, 3), substr($digits, 3, 3), substr($digits, 6)); $v[] = sprintf('%s %s %s', substr($digits, 0, 3), substr($digits, 3, 3), substr($digits, 6)); $v[] = '1' . $digits; $v[] = '+1' . $digits; $v[] = '+1 ' . sprintf('(%s) %s-%s', substr($digits, 0, 3), substr($digits, 3, 3), substr($digits, 6)); $v[] = '+1-' . sprintf('%s-%s-%s', substr($digits, 0, 3), substr($digits, 3, 3), substr($digits, 6)); } elseif (strlen($digits) === 11 && str_starts_with($digits, '1')) { $ten = substr($digits, 1); $v[] = $ten; $v[] = sprintf('(%s)-%s-%s', substr($ten, 0, 3), substr($ten, 3, 3), substr($ten, 6)); $v[] = sprintf('%s-%s-%s', substr($ten, 0, 3), substr($ten, 3, 3), substr($ten, 6)); $v[] = sprintf('%s %s %s', substr($ten, 0, 3), substr($ten, 3, 3), substr($ten, 6)); $v[] = '+1' . $ten; $v[] = '+1 ' . sprintf('(%s) %s-%s', substr($ten, 0, 3), substr($ten, 3, 3), substr($ten, 6)); $v[] = '+1-' . sprintf('%s-%s-%s', substr($ten, 0, 3), substr($ten, 3, 3), substr($ten, 6)); } else { $v[] = $digits; // partials } } if (!empty($v)) $phoneMap[$t] = array_values(array_unique($v)); } // Helper $applyMultiTokenLike = function ($qb, array $columns, array $tokens, array $phoneCols = []) use ($phoneMap) { foreach ($tokens as $t) { $qb->groupStart(); foreach ($columns as $i => $col) { if ($i === 0) $qb->like($col, $t); else $qb->orLike($col, $t); } if (!empty($phoneMap[$t]) && !empty($phoneCols)) { foreach ($phoneMap[$t] as $pv) { foreach ($phoneCols as $pcol) { $qb->orLike($pcol, $pv); } } } $qb->groupEnd(); } return $qb; }; // USERS (phone: cellphone) $uCols = ['firstname', 'lastname', 'email', 'cellphone', 'school_id', 'city', 'state']; $uQB = $db->table('users')->select('id, firstname, lastname, email, cellphone, school_id, city, state, school_year, semester'); $applyMultiTokenLike($uQB, $uCols, $tokens, ['cellphone']); $users = $uQB->limit(150)->get()->getResultArray(); // STUDENTS (no phone field) $sCols = ['firstname', 'lastname', 'school_id', 'rfid_tag', 'dob', 'gender']; $sQB = $db->table('students')->select('id, parent_id, school_id, firstname, lastname, dob, gender, school_year, semester, rfid_tag'); $applyMultiTokenLike($sQB, $sCols, $tokens, []); $students = $sQB->limit(150)->get()->getResultArray(); // PARENTS (phone: secondparent_phone) $pCols = ['secondparent_firstname', 'secondparent_lastname', 'secondparent_email', 'secondparent_phone']; $pQB = $db->table('parents')->select('id, firstparent_id, secondparent_firstname, secondparent_lastname, secondparent_email, secondparent_phone, school_year, semester'); $applyMultiTokenLike($pQB, $pCols, $tokens, ['secondparent_phone']); foreach ($tokens as $t) { if (ctype_digit($t)) { $pQB->orWhere('firstparent_id', (int)$t)->orWhere('id', (int)$t); } } $parents = $pQB->limit(150)->get()->getResultArray(); // STAFF (phone: phone) $stCols = ['firstname', 'lastname', 'email', 'role_name', 'phone']; $stQB = $db->table('staff')->select('id, user_id, firstname, lastname, email, phone, role_name, school_year, active_role'); $applyMultiTokenLike($stQB, $stCols, $tokens, ['phone']); $staff = $stQB->limit(150)->get()->getResultArray(); // EMERGENCY CONTACTS (phone: cellphone) $ecCols = ['emergency_contact_name', 'relation', 'email', 'cellphone']; $ecQB = $db->table('emergency_contacts')->select('id, parent_id, emergency_contact_name, relation, cellphone, email, school_year, semester'); $applyMultiTokenLike($ecQB, $ecCols, $tokens, ['cellphone']); $emergency = $ecQB->limit(150)->get()->getResultArray(); $results = [ 'users' => $users, 'students' => $students, 'parents' => $parents, 'staff' => $staff, 'emergency_contacts' => $emergency, 'total' => count($users) + count($students) + count($parents) + count($staff) + count($emergency), ]; return $this->success($results, 'Search completed successfully'); } catch (\Throwable $e) { log_message('error', 'Search error: ' . $e->getMessage()); return $this->error('Failed to perform search', 500); } } /** * Get enrollment/withdrawal data * GET /api/v1/administrator/enrollment-withdrawal */ public function enrollmentWithdrawal() { $page = (int) ($this->request->getGet('page') ?? 1); $perPage = min(100, (int) ($this->request->getGet('per_page') ?? 20)); $schoolYear = $this->request->getGet('school_year') ?? $this->schoolYear; try { $query = $this->enrollment ->where('school_year', $schoolYear) ->orderBy('created_at', 'DESC'); $result = $this->paginate($query, $page, $perPage); return $this->success($result, 'Enrollment/withdrawal data retrieved successfully'); } catch (\Exception $e) { log_message('error', 'Enrollment withdrawal error: ' . $e->getMessage()); return $this->error('Failed to retrieve enrollment/withdrawal data', 500); } } /** * GET /api/v1/administrator/enrollment-withdrawal/data * Rich data used by admin UI: students, classes, school years */ public function enrollmentWithdrawalData() { try { $selectedYear = trim((string)($this->request->getGet('schoolYear') ?? '')); if ($selectedYear === '') { $selectedYear = (string)$this->schoolYear; } // Distinct school years $yearsRows = $this->db->table('enrollments')->distinct()->select('school_year')->orderBy('school_year', 'DESC')->get()->getResultArray(); $schoolYears = array_values(array_filter(array_map(static fn($r) => isset($r['school_year']) ? (string)$r['school_year'] : null, $yearsRows))); $students = $this->student->getStudentsWithClassAndEnrollment(); foreach ($students as &$s) { $s['student_id'] = (int)($s['id'] ?? 0); if (empty($s['parent_id']) && !empty($s['secondparent_user_id'])) { $s['parent_id'] = (int)$s['secondparent_user_id']; } else { $s['parent_id'] = (int)($s['parent_id'] ?? 0); } $pf = trim((string)($s['parent_firstname'] ?? '')); $pl = trim((string)($s['parent_lastname'] ?? '')); if ($pf === '' && $pl === '' && !empty($s['parent_fullname'])) { $parts = preg_split('/\s+/', trim((string)$s['parent_fullname']), 2); $pf = $parts[0] ?? ''; $pl = $parts[1] ?? ''; } $s['parent_label'] = trim($pf . ' ' . $pl) ?: 'Unknown Parent'; $s['parent_sort'] = trim(($pl !== '' ? $pl : $pf) . ' ' . $pf) ?: 'ZZZ Unknown Parent'; $s['is_new'] = (int) ($s['is_new'] ?? 0); $s['new_student'] = $s['is_new'] === 1 ? 'Yes' : 'No'; $statusForYear = $this->enrollment->getEnrollmentStatus((int)$s['student_id'], $selectedYear); if (!empty($statusForYear)) { $s['enrollment_status'] = $statusForYear; } elseif (($s['admission_status'] ?? null) === 'denied') { $s['enrollment_status'] = 'denied'; } $className = $this->studentClass->getClassSectionsByStudentId((int)$s['student_id'], $selectedYear); $s['class_section'] = $className ?: 'Class not Assigned'; $s['registration_date_order'] = !empty($s['registration_date']) ? date('Y-m-d', strtotime($s['registration_date'])) : ''; } unset($s); usort($students, function (array $a, array $b) { $pa = $a['parent_sort'] ?? ''; $pb = $b['parent_sort'] ?? ''; if (strcasecmp($pa, $pb) === 0) { $la = $a['lastname'] ?? ''; $lb = $b['lastname'] ?? ''; $cmp = strcasecmp($la, $lb); if ($cmp !== 0) return $cmp; return strcasecmp($a['firstname'] ?? '', $b['firstname'] ?? ''); } return strcasecmp($pa, $pb); }); $classes = $this->classSection ->select('id, class_section_id, class_section_name, school_year, semester') ->where('school_year', (string)$selectedYear) ->where('semester', (string)$this->semester) ->orderBy('class_section_name', 'ASC') ->findAll(); if (empty($classes)) { $classes = $this->classSection ->select('id, class_section_id, class_section_name') ->orderBy('class_section_name', 'ASC') ->findAll(); } return $this->success([ 'students' => $students, 'classes' => $classes, 'semester' => (string)$this->semester, 'school_year' => (string)$selectedYear, 'schoolYears' => $schoolYears, ], 'Enrollment/withdrawal data'); } catch (\Throwable $e) { log_message('error', 'enrollmentWithdrawalData error: {msg}', ['msg' => $e->getMessage()]); return $this->error('Server error', 500); } } /** * POST /api/v1/administrator/enrollment-withdrawal/update * Update enrollment statuses in batch and trigger notifications/refunds * Payload: { enrollment_status: { student_id: status, ... } } */ public function updateEnrollmentStatuses() { $payload = $this->request->getJSON(true) ?? []; $enrollmentStatuses = $payload['enrollment_status'] ?? null; if (empty($enrollmentStatuses) || !is_array($enrollmentStatuses)) { return $this->error('No enrollment statuses were submitted.', 422); } $refundService = new FeeCalculationService(); $this->db->transStart(); try { $errors = []; $groupsByParentStatus = []; $parentInfo = []; $refundParents = []; $refundAmountByParent = []; $validStatuses = [ 'admission under review', 'payment pending', 'enrolled', 'withdraw under review', 'refund pending', 'withdrawn', 'denied', 'waitlist', ]; foreach ($enrollmentStatuses as $studentId => $newEnrollmentStatus) { if (!in_array($newEnrollmentStatus, $validStatuses, true)) { $errors[] = "Invalid enrollment status '$newEnrollmentStatus' for student ID $studentId."; continue; } if ($newEnrollmentStatus === 'denied') { $admissionStatus = 'denied'; } elseif (in_array($newEnrollmentStatus, ['enrolled', 'payment pending'], true)) { $admissionStatus = 'accepted'; } else { $admissionStatus = 'pending'; } $enrollmentRow = $this->db->table('enrollments') ->where('student_id', $studentId) ->where('school_year', $this->schoolYear) ->get() ->getRowArray(); if (!$enrollmentRow) { $stu = $this->student->find((int)$studentId) ?? []; $parentId = (int)($stu['parent_id'] ?? ($stu['secondparent_user_id'] ?? 0)); if (!$parentId) { $errors[] = "No parent ID found for student ID $studentId."; continue; } $isWithdrawn = in_array($newEnrollmentStatus, ['withdrawn', 'refund pending', 'withdraw under review'], true) ? 1 : 0; $ok = $this->db->table('enrollments')->insert([ 'student_id' => (int)$studentId, 'parent_id' => $parentId, 'school_year' => (string)$this->schoolYear, 'semester' => (string)$this->semester, 'enrollment_date' => local_date(utc_now(), 'Y-m-d'), 'is_withdrawn' => $isWithdrawn, 'enrollment_status' => $newEnrollmentStatus, 'admission_status' => $admissionStatus, 'created_at' => utc_now(), 'updated_at' => utc_now(), ]); if (!$ok) { $errors[] = "Failed to create enrollment for student ID $studentId."; continue; } $studentRow = $this->student->find($studentId) ?? []; $studentName = trim(($studentRow['firstname'] ?? '') . ' ' . ($studentRow['lastname'] ?? '')) ?: "Student #{$studentId}"; if (!isset($parentInfo[$parentId])) { $p = $this->user->find($parentId) ?? []; $parentInfo[$parentId] = [ 'user_id' => $p['id'] ?? $parentId, 'email' => $p['email'] ?? null, 'firstname' => $p['firstname'] ?? '', 'lastname' => $p['lastname'] ?? '', ]; } $groupsByParentStatus[$parentId][$newEnrollmentStatus][] = [ 'student_id' => (int) $studentId, 'student_name' => $studentName, ]; if ($newEnrollmentStatus === 'refund pending') { $refundParents[$parentId] = true; } continue; } $oldStatus = $enrollmentRow['enrollment_status'] ?? null; $parentId = $enrollmentRow['parent_id'] ?? null; if (!$parentId) { $errors[] = "No parent ID found for student ID $studentId."; continue; } if ($oldStatus === $newEnrollmentStatus) { continue; } $updated = $this->db->table('enrollments') ->where('student_id', $studentId) ->where('school_year', $this->schoolYear) ->update([ 'enrollment_status' => $newEnrollmentStatus, 'admission_status' => $admissionStatus, 'updated_at' => utc_now(), ]); if (!$updated) { $errors[] = "Failed to update enrollment for student ID $studentId."; continue; } $studentRow = $this->student->find($studentId); $studentName = trim(($studentRow['firstname'] ?? '') . ' ' . ($studentRow['lastname'] ?? '')) ?: "Student #{$studentId}"; if (!isset($parentInfo[$parentId])) { $p = $this->user->find($parentId) ?? []; $parentInfo[$parentId] = [ 'user_id' => $p['id'] ?? $parentId, 'email' => $p['email'] ?? null, 'firstname' => $p['firstname'] ?? '', 'lastname' => $p['lastname'] ?? '', ]; } $groupsByParentStatus[$parentId][$newEnrollmentStatus][] = [ 'student_id' => (int) $studentId, 'student_name' => $studentName, ]; if ($newEnrollmentStatus === 'refund pending') { $refundParents[$parentId] = true; } } // Refunds foreach (array_keys($refundParents) as $pid) { $students = $this->enrollment ->where('parent_id', $pid) ->where('school_year', $this->schoolYear) ->findAll(); if (empty($students)) { continue; } $invoice = $this->invoice->where('parent_id', $pid) ->where('school_year', $this->schoolYear) ->orderBy('created_at', 'DESC') ->first(); if (!$invoice) { $errors[] = "No invoice found for parent ID $pid (for refund calc)."; continue; } $refundAmount = $refundService->calculateRefund($students, $pid); $refundAmountByParent[$pid] = $refundAmount; $existingRefund = $this->refund->where('invoice_id', $invoice['id'])->first(); if ($existingRefund) { $this->refund->update($existingRefund['id'], [ 'refund_amount' => $refundAmount, 'status' => 'Pending', 'updated_by' => $this->getCurrentUserId(), ]); } else { $this->refund->insert([ 'parent_id' => $pid, 'school_year' => $invoice['school_year'], 'invoice_id' => $invoice['id'], 'refund_amount' => $refundAmount, 'refund_paid_amount' => 0.0, 'status' => 'Pending', 'requested_at' => utc_now(), 'updated_by' => $this->getCurrentUserId(), ]); } } $this->db->transComplete(); if (!$this->db->transStatus()) { return $this->error('A database error occurred. Changes were rolled back.', 500); } // Build response summary return $this->success([ 'errors' => $errors, 'groupsByParentStatus' => $groupsByParentStatus, ], empty($errors) ? 'Enrollment statuses updated.' : 'Enrollment updated with some errors.'); } catch (\Throwable $e) { $this->db->transRollback(); log_message('error', 'Enrollment withdrawal error: ' . $e->getMessage()); return $this->error('An unexpected error occurred while processing enrollments.', 500); } } /** * GET /api/v1/administrator/students * Return student profiles with parent/emergency and class section name */ public function studentProfiles() { $db = $this->db; $isPg = ($db->getPlatform() === 'Postgre'); if (!$isPg) { $db->query('SET SESSION group_concat_max_len = 8192'); } $b = $db->table('students'); if ($isPg) { $students = $b->select([ 'students.*', 'users.firstname AS parent_firstname', 'users.lastname AS parent_lastname', 'users.email AS parent_email', 'users.cellphone AS parent_phone', "(SELECT ec.emergency_contact_name FROM emergency_contacts ec WHERE ec.parent_id = students.parent_id ORDER BY ec.id ASC LIMIT 1) AS emergency_name", "(SELECT ec.relation FROM emergency_contacts ec WHERE ec.parent_id = students.parent_id ORDER BY ec.id ASC LIMIT 1) AS emergency_relationship", "(SELECT ec.cellphone FROM emergency_contacts ec WHERE ec.parent_id = students.parent_id ORDER BY ec.id ASC LIMIT 1) AS emergency_phone", "(SELECT ec.email FROM emergency_contacts ec WHERE ec.parent_id = students.parent_id ORDER BY ec.id ASC LIMIT 1) AS emergency_email", "(SELECT STRING_AGG(DISTINCT sa.allergy, ', ' ORDER BY sa.allergy) FROM student_allergies sa WHERE sa.student_id = students.id) AS allergies", "(SELECT STRING_AGG(DISTINCT smc.condition_name, ', ' ORDER BY smc.condition_name) FROM student_medical_conditions smc WHERE smc.student_id = students.id) AS medical_conditions", ])->join('users', 'users.id = students.parent_id', 'left') ->orderBy('students.lastname', 'ASC')->orderBy('students.firstname', 'ASC') ->get()->getResultArray(); } else { $students = $b->select([ 'students.*', 'users.firstname AS parent_firstname', 'users.lastname AS parent_lastname', 'users.email AS parent_email', 'users.cellphone AS parent_phone', 'MIN(emergency_contacts.emergency_contact_name) AS emergency_name', 'MIN(emergency_contacts.relation) AS emergency_relationship', 'MIN(emergency_contacts.cellphone) AS emergency_phone', 'MIN(emergency_contacts.email) AS emergency_email', "GROUP_CONCAT(DISTINCT student_allergies.allergy ORDER BY student_allergies.allergy SEPARATOR ', ') AS allergies", "GROUP_CONCAT(DISTINCT student_medical_conditions.condition_name ORDER BY student_medical_conditions.condition_name SEPARATOR ', ') AS medical_conditions", ])->join('users', 'users.id = students.parent_id', 'left') ->join('emergency_contacts', 'emergency_contacts.parent_id = students.parent_id', 'left') ->join('student_allergies', 'student_allergies.student_id = students.id', 'left') ->join('student_medical_conditions', 'student_medical_conditions.student_id = students.id', 'left') ->groupBy('students.id') ->orderBy('students.lastname', 'ASC')->orderBy('students.firstname', 'ASC') ->get()->getResultArray(); } foreach ($students as $i => $row) { $sid = (int) ($row['id'] ?? 0); $students[$i]['class_section_name'] = $sid > 0 ? (string) ($this->studentClass->getClassSectionNameByStudentId($sid) ?? '') : ''; } return $this->success(['students' => $students]); } /** * GET /api/v1/administrator/parents * Return parent profiles with latest invoice paid/balance */ public function parentProfiles() { $allUsers = $this->user->findAll(); $parents = []; foreach ($allUsers as $user) { $roles = $this->userRole->getRolesByUserId($user['id']); $isParent = false; if (is_array($roles)) { foreach ($roles as $role) { if (isset($role['role_name']) && $role['role_name'] === 'parent') { $isParent = true; break; } } } if ($isParent) { $paidAmount = $this->invoice->getLatestInvoicePaidAmount($user['id']) ?? 0; $balance = $this->invoice->getLatestInvoiceBalance($user['id']) ?? 0; $parents[] = [ 'id' => $user['id'], 'school_id' => $user['school_id'], 'firstname' => $user['firstname'], 'lastname' => $user['lastname'], 'email' => $user['email'], 'cellphone' => $user['cellphone'], 'gender' => $user['gender'], 'created_at' => $user['created_at'], 'school_year' => $user['school_year'], 'paid_amount' => $paidAmount, 'balance' => $balance, ]; } } return $this->success(['parents' => $parents]); } /** * GET /api/v1/administrator/enrollment/new-students * Return students marked as new with parent/emergency + enrollment metadata. */ public function newStudents() { try { $rows = $this->student->getStudentsWithParentsAndEmergency($this->schoolYear, 1); $newStudents = []; foreach ($rows as $row) { $studentId = (int) ($row['id'] ?? 0); $classSection = $studentId > 0 ? ($this->studentClass->getClassSectionsByStudentId($studentId, $this->schoolYear) ?: 'Class not Assigned') : 'Class not Assigned'; $enrollmentStatus = $studentId > 0 ? $this->enrollment->getEnrollmentStatus($studentId, $this->schoolYear) : null; $registrationDate = $row['registration_date'] ?? null; if (!empty($registrationDate)) { try { $registrationDate = (new \DateTime($registrationDate))->format('Y-m-d'); } catch (\Throwable $e) { $registrationDate = null; } } $newStudents[] = array_merge($row, [ 'student_id' => $studentId, 'class_section' => $classSection, 'enrollment_status' => $enrollmentStatus, 'new_student' => ((int) ($row['is_new'] ?? 0) === 1) ? 'Yes' : 'No', 'modalIdContact' => 'contact_' . $studentId, 'registration_date' => $registrationDate, ]); } return $this->success([ 'new_students' => $newStudents, 'total_new' => count($newStudents), 'school_year' => $this->schoolYear, 'semester' => $this->semester, ], 'New students retrieved successfully'); } catch (\Throwable $e) { log_message('error', 'newStudents error: ' . $e->getMessage()); return $this->error('Failed to fetch new students', 500); } } }