db = $db; $this->userModel = $userModel; $this->loginActivityModel = $loginActivityModel; } public function metrics(string $schoolYear, string $semester): array { $this->schoolYear = $schoolYear; $this->semester = $semester; $recentActivities = $this->loginActivityModel->getLastActivities(4); if (!is_array($recentActivities)) { $recentActivities = []; } $totalAdmins = (int) ($this->userModel->countAdminsBySchoolYear($this->schoolYear) ?? 0); $teachers = $this->userModel->getUsersByRole('teacher'); $totalTeachers = $this->countUniqueEntities($teachers); $teacherAssistants = $this->userModel->getUsersByRole('teacher_assistant'); $totalTeacherAssistants = $this->countUniqueEntities($teacherAssistants); $totalParents = $this->countParentsWithEnrolledStudents($this->schoolYear); // Count only students that have a class assigned and exist in student_class for the current school year $totalStudents = (int) ( $this->db->table('student_class') ->select('COUNT(DISTINCT student_class.student_id) AS cnt') ->join('students', 'students.id = student_class.student_id', 'inner') ->where('student_class.school_year', $this->schoolYear) ->where('student_class.class_section_id IS NOT NULL', null, false) ->where('students.is_active', 1) ->get() ->getRow('cnt') ?? 0 ); return [ '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, ], ]; } private function countUniqueEntities($rows): int { if (!is_array($rows) || $rows === []) { return 0; } $ids = []; foreach ($rows as $row) { if (!is_array($row)) { continue; } if (isset($row['id'])) { $ids[] = (int) $row['id']; continue; } if (isset($row['user_id'])) { $ids[] = (int) $row['user_id']; } } return count(array_unique($ids)); } private function countParentsWithEnrolledStudents(string $schoolYear): int { $schoolYear = trim($schoolYear); if ($schoolYear === '') { return 0; } return (int) ( $this->db->table('students') ->select('COUNT(DISTINCT students.parent_id) AS cnt') ->join('student_class', 'student_class.student_id = students.id', 'inner') ->join('users', 'users.id = students.parent_id', 'inner') ->join('user_roles', 'user_roles.user_id = users.id', 'inner') ->join('roles', 'roles.id = user_roles.role_id', 'inner') ->where('student_class.school_year', $schoolYear) ->where('student_class.class_section_id IS NOT NULL', null, false) ->where('students.is_active', 1) ->where('students.parent_id IS NOT NULL', null, false) ->where('students.parent_id >', 0) ->where('user_roles.deleted_at', null) ->groupStart() ->where('LOWER(roles.name)', 'parent') ->orWhere('roles.slug', 'parent') ->groupEnd() ->get() ->getRow('cnt') ?? 0 ); } public function search(string $query): array { $q = trim($query); if ($q === '') { return [ 'query' => '', 'results' => [], 'scope_used' => 'unscoped-raw', 'scope_label' => 'all years/semesters (raw)', 'total_found' => 0, ]; } $db = $this->db; // 1) Tokenize input: split by whitespace and punctuation, keep meaningful pieces $rawTokens = preg_split('/[,\s]+/u', $q, -1, PREG_SPLIT_NO_EMPTY) ?: []; $tokens = array_values(array_filter(array_map('trim', $rawTokens))); if ($tokens === []) { return [ 'query' => $q, 'results' => [], 'scope_used' => 'unscoped-merged', 'scope_label' => 'all years/semesters (merged)', 'total_found' => 0, ]; } // 2) Build phone variants for any token that looks numeric-ish $phoneMap = []; // token => variants[] foreach ($tokens as $t) { $digits = preg_replace('/\D+/', '', $t); if ($digits === '') { continue; } $v = []; if (strlen($digits) >= 7) { // base forms $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)); // country-code forms $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 { // 7-9 digits: keep as-is (partial phone fragment) $v[] = $digits; } } if (!empty($v)) { $phoneMap[$t] = array_values(array_unique($v)); } } // Helper: ( token1 AND token2 AND ... ), each token may match ANY of $columns, // and phone variants are only applied to the provided $phoneCols for THIS table. $applyMultiTokenLike = function ($qb, array $columns, array $tokens, array $phoneCols = []) use ($phoneMap) { foreach ($tokens as $t) { $qb->groupStart(); // OR across columns for this token 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; }; // ===== RAW UNscoped searches (flat arrays) ===== // USERS (phone col: cellphone) $uCols = ['firstname', 'lastname', 'email', 'cellphone', 'school_id', 'city', 'state']; $uQB = $db->table('users') ->select('id, firstname, lastname, email, cellphone, school_id, city, state'); $applyMultiTokenLike($uQB, $uCols, $tokens, ['cellphone']); $users = $uQB->limit(150)->get()->getResultArray(); // STUDENTS (no phone column to search) $sCols = ['firstname', 'lastname', 'school_id', 'rfid_tag', 'dob', 'gender']; $sQB = $db->table('students') ->select('id, parent_id, school_id, firstname, lastname, dob, gender, rfid_tag'); $applyMultiTokenLike($sQB, $sCols, $tokens, []); $students = $sQB->limit(150)->get()->getResultArray(); // PARENTS (phone col: 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'); $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 col: phone) $stCols = ['firstname', 'lastname', 'email', 'role_name', 'phone']; $stQB = $db->table('staff') ->select('id, user_id, firstname, lastname, email, phone, role_name, active_role'); $applyMultiTokenLike($stQB, $stCols, $tokens, ['phone']); $staff = $stQB->limit(150)->get()->getResultArray(); // EMERGENCY CONTACTS (phone col: cellphone) $ecCols = ['emergency_contact_name', 'relation', 'email', 'cellphone']; $ecQB = $db->table('emergency_contacts') ->select('id, parent_id, emergency_contact_name, relation, cellphone, email'); $applyMultiTokenLike($ecQB, $ecCols, $tokens, ['cellphone']); $emergency = $ecQB->limit(150)->get()->getResultArray(); $results = $this->mergeSearchResults($users, $students, $parents, $staff, $emergency); return [ 'query' => $q, 'results' => $results, 'scope_used' => 'unscoped-merged', 'scope_label' => 'all years/semesters (merged, tokenized)', 'total_found' => count($results), ]; } private function mergeSearchResults(array $users, array $students, array $parents, array $staff, array $emergency): array { $bundles = []; $userIds = []; $ensureBundle = static function (int $userId) use (&$bundles, &$userIds): void { if ($userId <= 0) { return; } if (!isset($bundles[$userId])) { $bundles[$userId] = [ 'user' => null, 'students' => [], 'parents' => [], 'staff' => [], 'emergency_contacts' => [], ]; } $userIds[$userId] = $userId; }; foreach ($users as $user) { $userId = (int) ($user['id'] ?? 0); $ensureBundle($userId); if ($userId > 0) { $bundles[$userId]['user'] = $user; } } foreach ($students as $student) { $parentId = (int) ($student['parent_id'] ?? 0); $ensureBundle($parentId); if ($parentId > 0) { $bundles[$parentId]['students'][(int) ($student['id'] ?? 0)] = $student; } } foreach ($parents as $parent) { $firstParentId = (int) ($parent['firstparent_id'] ?? 0); $ensureBundle($firstParentId); if ($firstParentId > 0) { $bundles[$firstParentId]['parents'][(int) ($parent['id'] ?? 0)] = $parent; } } foreach ($staff as $staffRow) { $userId = (int) ($staffRow['user_id'] ?? 0); $ensureBundle($userId); if ($userId > 0) { $bundles[$userId]['staff'][(int) ($staffRow['id'] ?? 0)] = $staffRow; } } foreach ($emergency as $emergencyRow) { $parentId = (int) ($emergencyRow['parent_id'] ?? 0); $ensureBundle($parentId); if ($parentId > 0) { $bundles[$parentId]['emergency_contacts'][(int) ($emergencyRow['id'] ?? 0)] = $emergencyRow; } } if ($userIds === []) { return []; } $this->hydrateSearchBundles($bundles, array_values($userIds)); $results = array_values($bundles); usort($results, static function (array $a, array $b): int { $aUser = $a['user'] ?? []; $bUser = $b['user'] ?? []; $aName = trim((string) ($aUser['lastname'] ?? '') . ' ' . (string) ($aUser['firstname'] ?? '')); $bName = trim((string) ($bUser['lastname'] ?? '') . ' ' . (string) ($bUser['firstname'] ?? '')); return strcasecmp($aName, $bName); }); return $results; } private function hydrateSearchBundles(array &$bundles, array $userIds): void { $userRows = $this->db->table('users') ->select('id, firstname, lastname, email, cellphone, school_id, city, state') ->whereIn('id', $userIds) ->get() ->getResultArray(); foreach ($userRows as $user) { $userId = (int) ($user['id'] ?? 0); if ($userId > 0 && isset($bundles[$userId]) && empty($bundles[$userId]['user'])) { $bundles[$userId]['user'] = $user; } } $studentRows = $this->db->table('students') ->select('id, parent_id, school_id, firstname, lastname, dob, gender, rfid_tag, is_active') ->whereIn('parent_id', $userIds) ->orderBy('lastname', 'ASC') ->orderBy('firstname', 'ASC') ->get() ->getResultArray(); foreach ($studentRows as $student) { $parentId = (int) ($student['parent_id'] ?? 0); if ($parentId > 0 && isset($bundles[$parentId])) { $bundles[$parentId]['students'][(int) ($student['id'] ?? 0)] = $student; } } $parentRows = $this->db->table('parents') ->select('id, firstparent_id, secondparent_firstname, secondparent_lastname, secondparent_email, secondparent_phone') ->whereIn('firstparent_id', $userIds) ->get() ->getResultArray(); foreach ($parentRows as $parent) { $firstParentId = (int) ($parent['firstparent_id'] ?? 0); if ($firstParentId > 0 && isset($bundles[$firstParentId])) { $bundles[$firstParentId]['parents'][(int) ($parent['id'] ?? 0)] = $parent; } } $staffRows = $this->db->table('staff') ->select('id, user_id, firstname, lastname, email, phone, role_name, active_role') ->whereIn('user_id', $userIds) ->get() ->getResultArray(); foreach ($staffRows as $staffRow) { $userId = (int) ($staffRow['user_id'] ?? 0); if ($userId > 0 && isset($bundles[$userId])) { $bundles[$userId]['staff'][(int) ($staffRow['id'] ?? 0)] = $staffRow; } } $emergencyRows = $this->db->table('emergency_contacts') ->select('id, parent_id, emergency_contact_name, relation, cellphone, email') ->whereIn('parent_id', $userIds) ->get() ->getResultArray(); foreach ($emergencyRows as $emergencyRow) { $parentId = (int) ($emergencyRow['parent_id'] ?? 0); if ($parentId > 0 && isset($bundles[$parentId])) { $bundles[$parentId]['emergency_contacts'][(int) ($emergencyRow['id'] ?? 0)] = $emergencyRow; } } foreach ($bundles as &$bundle) { $bundle['students'] = array_values($bundle['students']); $bundle['parents'] = array_values($bundle['parents']); $bundle['staff'] = array_values($bundle['staff']); $bundle['emergency_contacts'] = array_values($bundle['emergency_contacts']); } unset($bundle); } }