441 lines
16 KiB
PHP
441 lines
16 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use App\Models\LoginActivityModel;
|
|
use App\Models\UserModel;
|
|
|
|
class AdministratorDashboardService
|
|
{
|
|
protected $db;
|
|
protected $userModel;
|
|
protected $loginActivityModel;
|
|
protected string $schoolYear = '';
|
|
protected string $semester = '';
|
|
|
|
public function __construct(
|
|
\CodeIgniter\Database\BaseConnection $db,
|
|
UserModel $userModel,
|
|
LoginActivityModel $loginActivityModel
|
|
) {
|
|
$this->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);
|
|
}
|
|
}
|