Files
alrahma_sunday_school/app/Services/AdministratorDashboardService.php
T
root 2b0206e7f2
Deploy to Shared Hosting / Shared hosting deploy (push) Failing after 47s
Tests / PHPUnit (push) Successful in 1m16s
move service logic from grading and administrator controller
2026-08-20 16:59:51 -04:00

251 lines
9.3 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->getUsersByRoleAndSchoolYear('teacher', $this->schoolYear);
$totalTeachers = $this->countUniqueEntities($teachers);
$teacherAssistants = $this->userModel->getUsersByRoleAndSchoolYear('teacher_assistant', $this->schoolYear);
$totalTeacherAssistants = $this->countUniqueEntities($teacherAssistants);
$parents = $this->userModel->getUsersByRoleAndSchoolYear('parent', $this->schoolYear);
$totalParents = $this->countUniqueEntities($parents);
// 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));
}
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)));
// 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();
$raw = [
'users' => $users,
'students' => $students,
'parents' => $parents,
'staff' => $staff,
'emergency_contacts' => $emergency,
];
$total = count($users) + count($students) + count($parents) + count($staff) + count($emergency);
return [
'query' => $q,
'results' => $raw,
'scope_used' => 'unscoped-raw',
'scope_label' => 'all years/semesters (raw, tokenized)',
'total_found' => $total,
];
}
}