diff --git a/app/Config/Services.php b/app/Config/Services.php index 85def68..eb8acf4 100644 --- a/app/Config/Services.php +++ b/app/Config/Services.php @@ -234,6 +234,20 @@ class Services extends BaseService ); } + public static function staffDirectorySync(bool $getShared = true): \App\Services\StaffDirectorySyncService + { + if ($getShared) { + return static::getSharedInstance('staffDirectorySync'); + } + + return new \App\Services\StaffDirectorySyncService( + model(\App\Models\StaffModel::class), + model(\App\Models\UserModel::class), + model(\App\Models\ConfigurationModel::class), + \Config\Database::connect() + ); + } + public static function schoolYearManagement(bool $getShared = true): \App\Services\SchoolYearManagementService { if ($getShared) { diff --git a/app/Config/SessionTimeout.php b/app/Config/SessionTimeout.php index c98212d..52e4e56 100644 --- a/app/Config/SessionTimeout.php +++ b/app/Config/SessionTimeout.php @@ -7,12 +7,12 @@ class SessionTimeout // Session timeout in seconds (30 minutes) public const TIMEOUT_DURATION = 1800; - // Show warning after 25 minutes of inactivity (5 minutes before timeout) - public const WARNING_THRESHOLD = 1500; + // Show warning during the last 30 seconds before timeout. + public const WARNING_THRESHOLD = 1770; // Server-side check interval (in seconds) - public const CHECK_INTERVAL = 60; // 1 minute + public const CHECK_INTERVAL = 5; // Client-side check interval (in milliseconds) - public const CLIENT_CHECK_INTERVAL = 60000; // 1 minute + public const CLIENT_CHECK_INTERVAL = 5000; } diff --git a/app/Controllers/Administrator/SchoolYearController.php b/app/Controllers/Administrator/SchoolYearController.php index 7f9f897..97078c9 100644 --- a/app/Controllers/Administrator/SchoolYearController.php +++ b/app/Controllers/Administrator/SchoolYearController.php @@ -48,6 +48,7 @@ class SchoolYearController extends BaseController 'statuses' => SchoolYearStatus::ALL, 'activeYear' => $activeYear, 'nextDraftYear' => $nextDraftYear, + 'nextDraftDefaults' => service('schoolYearManagement')->nextDraftDefaults(), 'closingYear' => $closingYear, 'archivedCount' => $archivedCount, 'latestTransitions' => service('schoolYearManagement')->latestTransitionByYear(), diff --git a/app/Controllers/View/AdministratorController.php b/app/Controllers/View/AdministratorController.php index 8ab2bc7..42e797a 100644 --- a/app/Controllers/View/AdministratorController.php +++ b/app/Controllers/View/AdministratorController.php @@ -2309,6 +2309,8 @@ class AdministratorController extends BaseController ]); } + service('staffDirectorySync')->syncUser((int) $userId); + return redirect()->to('/administrator/manage-users'); } diff --git a/app/Controllers/View/HomeworkTrackingController.php b/app/Controllers/View/HomeworkTrackingController.php index 5d58641..812d8ce 100644 --- a/app/Controllers/View/HomeworkTrackingController.php +++ b/app/Controllers/View/HomeworkTrackingController.php @@ -72,13 +72,18 @@ class HomeworkTrackingController extends BaseController } $limitToSemester = $this->hasTeacherAssignments($this->schoolYear, $this->semester); + $semesterVariants = $this->getSemesterVariants($this->semester); - // Aggregate homework presence + first entered date per class_section_id + homework_index + // Aggregate submitted homework per class_section_id + homework_index. + // Adding a homework column creates blank rows for every student, so only + // indexes with at least one non-null score count as submitted. $hwQ = $this->db->table('homework') - ->select('class_section_id, homework_index, MIN(created_at) AS first_created, COUNT(*) AS cnt') + ->select('class_section_id, homework_index') + ->select('MIN(CASE WHEN score IS NOT NULL THEN updated_at ELSE NULL END) AS first_submitted', false) + ->select('COUNT(score) AS scored_count', false) ->where('school_year', $this->schoolYear); - if ($limitToSemester && $this->semester !== '') { - $hwQ->where('semester', $this->semester); + if ($limitToSemester && $semesterVariants !== []) { + $hwQ->whereIn('semester', $semesterVariants); } $rows = $hwQ->groupBy('class_section_id, homework_index') ->get() @@ -90,60 +95,32 @@ class HomeworkTrackingController extends BaseController foreach ($rows as $r) { $csid = (int)($r['class_section_id'] ?? 0); $hi = (int)($r['homework_index'] ?? 0); - $cnt = (int)($r['cnt'] ?? 0); + $cnt = (int)($r['scored_count'] ?? 0); if ($csid > 0 && $hi > 0 && $cnt > 0) { $hasHomework[$csid][$hi] = true; - $dateStr = substr((string)($r['first_created'] ?? ''), 0, 10); + $dateStr = substr((string)($r['first_submitted'] ?? ''), 0, 10); $hwEnteredAt[$csid][$hi] = $dateStr ?: null; $homeworkSubmissionCounts[$csid] = ($homeworkSubmissionCounts[$csid] ?? 0) + 1; } } - // Build date-based presence mapped to the nearest prior non-NoSchool Sunday. - $hb = $this->db->table('homework') - ->select("class_section_id, DATE(created_at) AS hw_date, MIN(created_at) AS first_created, COUNT(*) AS cnt", false) - ->where('school_year', $this->schoolYear); - if ($limitToSemester && $this->semester !== '') { - $hb->where('semester', $this->semester); + $indexToDate = []; + foreach ($dateToIndex as $ymd => $homeworkIndex) { + if ($homeworkIndex !== null) { + $indexToDate[(int) $homeworkIndex] = $ymd; + } } - $rowsByDate = $hb->groupBy('class_section_id, DATE(created_at)', '', false) - ->orderBy('hw_date', 'ASC', false) - ->get() - ->getResultArray(); - $hasHomeworkByDate = []; $hwEnteredAtByDate = []; - foreach ($rowsByDate as $r) { - $csid = (int)($r['class_section_id'] ?? 0); - $d = substr((string)($r['hw_date'] ?? ''), 0, 10); - $cnt = (int)($r['cnt'] ?? 0); - $firstCreated = substr((string)($r['first_created'] ?? ''), 0, 10); - if ($csid <= 0 || !$d || $cnt <= 0) { continue; } - - // Find the index of the last Sunday on or before $d - $baseIndex = -1; - for ($i = count($sundays) - 1; $i >= 0; $i--) { - if ($sundays[$i] <= $d) { $baseIndex = $i; break; } - } - if ($baseIndex < 0) { continue; } - - // Map this homework day to only the nearest prior non–No School Sunday - $j = $baseIndex; - while ($j >= 0 && !empty($eventDays[$sundays[$j]])) { $j--; } - if ($j < 0) { continue; } - $sd = $sundays[$j]; - - // Mark homework on that Sunday for this class_section (single mapping) - $hasHomeworkByDate[$csid][$sd] = true; - if (empty($hwEnteredAtByDate[$csid][$sd])) { - $hwEnteredAtByDate[$csid][$sd] = $firstCreated ?: $d; - } else { - // Keep the earliest date for display - $existing = (string)$hwEnteredAtByDate[$csid][$sd]; - $candidate = $firstCreated ?: $d; - if ($candidate && (!$existing || $candidate < $existing)) { - $hwEnteredAtByDate[$csid][$sd] = $candidate; + foreach ($hasHomework as $csid => $indexes) { + foreach ($indexes as $homeworkIndex => $_submitted) { + $ymd = $indexToDate[(int) $homeworkIndex] ?? null; + if ($ymd === null) { + continue; } + + $hasHomeworkByDate[(int) $csid][$ymd] = true; + $hwEnteredAtByDate[(int) $csid][$ymd] = $hwEnteredAt[(int) $csid][(int) $homeworkIndex] ?? null; } } @@ -252,6 +229,21 @@ class HomeworkTrackingController extends BaseController return $this->teacherAssignmentCache[$year]; } + private function getSemesterVariants(string $semester): array + { + $trimmed = trim($semester); + if ($trimmed === '') { + return []; + } + + return array_values(array_unique([ + $trimmed, + ucfirst(strtolower($trimmed)), + strtolower($trimmed), + strtoupper($trimmed), + ])); + } + /** * Compute start/end dates for the given semester within the school year. * Fall: 09/21/{startYear} to 01/18/{startYear+1} diff --git a/app/Controllers/View/RolePermissionController.php b/app/Controllers/View/RolePermissionController.php index cdb8c1d..7132385 100644 --- a/app/Controllers/View/RolePermissionController.php +++ b/app/Controllers/View/RolePermissionController.php @@ -9,6 +9,7 @@ use App\Models\UserModel; use App\Models\UserRoleModel; use App\Models\ConfigurationModel; use App\Models\StaffModel; +use App\Services\StaffDirectorySyncService; use CodeIgniter\Controller; class RolePermissionController extends Controller @@ -20,6 +21,7 @@ class RolePermissionController extends Controller protected $configModel; protected $permissionModel; protected $rolePermissionModel; + protected StaffDirectorySyncService $staffDirectorySync; protected $request; protected $db; protected $schoolYear; @@ -31,6 +33,7 @@ class RolePermissionController extends Controller $this->userModel = new UserModel(); $this->userRoleModel = new UserRoleModel(); $this->staffModel = new StaffModel(); + $this->staffDirectorySync = service('staffDirectorySync'); $this->configModel = new ConfigurationModel(); $this->permissionModel = new PermissionModel(); $this->rolePermissionModel = new RolePermissionModel(); @@ -224,49 +227,7 @@ class RolePermissionController extends Controller private function updateStaffRecord(int $userId, array $newRoleNames): void { - $excludedRoles = ['parent', 'student', 'guest']; - $loweredNewRoles = array_map('strtolower', $newRoleNames); - - $user = $this->userModel->find($userId); - if (!$user) { - log_message('error', "updateStaffRecord: user_id {$userId} not found"); - return; - } - - // Fetch existing staff record if it exists - $existingStaff = $this->staffModel->where('user_id', $userId)->first(); - - // Extract existing roles from DB if any - $existingRoles = []; - if (!empty($existingStaff['role_name'])) { - $existingRoles = array_map('strtolower', array_map('trim', explode(',', $existingStaff['role_name']))); - } - - // Merge and deduplicate roles - $allRoles = array_unique(array_merge($existingRoles, $loweredNewRoles)); - - // Determine staff roles (excluding parent/student/guest) - $staffRoles = array_filter($newRoleNames, fn($role) => !in_array($role, $excludedRoles)); - - // Determine active role - $activeRole = !empty($staffRoles) ? $staffRoles[0] : 'inactive'; - - $email = $this->generateUniqueStaffEmail($user['firstname'], $user['lastname'], $userId); - $now = utc_now(); - - $row = [ - 'user_id' => $userId, - 'firstname' => $user['firstname'], - 'lastname' => $user['lastname'], - 'email' => $email, - 'phone' => $user['cellphone'], - 'role_name' => implode(', ', $allRoles), // historical roles - 'active_role' => $activeRole, - 'school_year' => $this->schoolYear, - 'updated_at' => $now, - ]; - - $this->staffModel->upsert($row); + $this->staffDirectorySync->syncUser($userId); } private function generateUniqueStaffEmail(string $firstname, string $lastname, int $userId): string diff --git a/app/Controllers/View/StaffController.php b/app/Controllers/View/StaffController.php index 233ca79..7ac26ba 100644 --- a/app/Controllers/View/StaffController.php +++ b/app/Controllers/View/StaffController.php @@ -7,6 +7,7 @@ use App\Models\StaffModel; use App\Models\UserModel; use App\Models\TeacherClassModel; use App\Models\ConfigurationModel; +use App\Services\StaffDirectorySyncService; class StaffController extends BaseController { @@ -16,6 +17,7 @@ class StaffController extends BaseController protected $teacherClassModel; protected $userModel; protected $staffModel; + protected StaffDirectorySyncService $staffDirectorySync; public function __construct() { @@ -24,6 +26,7 @@ class StaffController extends BaseController $this->teacherClassModel = new TeacherClassModel(); $this->userModel = new UserModel(); $this->staffModel = new StaffModel(); + $this->staffDirectorySync = service('staffDirectorySync'); // Retrieve the configuration values $this->semester = $this->configModel->getConfig('semester'); @@ -38,61 +41,11 @@ class StaffController extends BaseController $schoolYear = $this->currentSchoolYearName((string) ($this->schoolYear ?? '')); - // roles we never show - $excludedRoles = ['student', 'parent', 'guest', 'inactive']; // ← add inactive here - - $staffList = $this->staffModel - ->whereNotIn('LOWER(active_role)', array_map('strtolower', $excludedRoles)) - ->orderBy('created_at', 'DESC') - ->findAll(); - - // Preload assignments for the selected school year (across all semesters), - // mirroring the logic used in teacher_class_assignment. - $assignRows = $this->teacherClassModel - ->select('teacher_class.teacher_id, teacher_class.position, classSection.class_section_name') - ->join('classSection', 'classSection.class_section_id = teacher_class.class_section_id', 'left') - ->where('teacher_class.school_year', $schoolYear) - ->findAll(); - - $assignByTeacher = []; - foreach ($assignRows as $r) { - $tid = (int)($r['teacher_id'] ?? 0); - if ($tid <= 0) { continue; } - $name = trim((string)($r['class_section_name'] ?? '')); - if ($name === '') { continue; } - $pos = strtolower((string)($r['position'] ?? '')); - if (!in_array($pos, ['main','ta'], true)) { continue; } - $assignByTeacher[$tid][] = $name . ' (' . $pos . ')'; - } - - $issuesCount = 0; - foreach ($staffList as &$staff) { - // attach school_id - $staff['school_id'] = $this->userModel->getSchoolIdByUserId($staff['user_id'] ?? null); - - // Verify and attach class assignments for teacher/TA roles for the selected school year - $role = strtolower((string)($staff['active_role'] ?? '')); - if (in_array($role, ['teacher', 'teacher_assistant'], true)) { - $tid = (int)($staff['user_id'] ?? 0); - $labels = $assignByTeacher[$tid] ?? []; - if (!empty($labels)) { - $staff['class_section'] = implode(', ', array_unique($labels)); - $staff['verification_issue'] = false; - } else { - $staff['class_section'] = 'No class assigned'; - $staff['verification_issue'] = true; - $issuesCount++; - } - } else { - $staff['class_section'] = '—'; - $staff['verification_issue'] = false; - } - } - unset($staff); // break reference + $directory = $this->staffDirectorySync->activeStaffForSchoolYear($schoolYear); return view('staff/index', [ - 'staff' => $staffList, - 'issues_count' => $issuesCount, + 'staff' => $directory['staff'], + 'issues_count' => $directory['issues_count'], 'semester' => $this->semester, 'school_year' => $schoolYear, ]); diff --git a/app/Controllers/View/UserController.php b/app/Controllers/View/UserController.php index 87d55da..15444e8 100644 --- a/app/Controllers/View/UserController.php +++ b/app/Controllers/View/UserController.php @@ -15,6 +15,7 @@ use App\Models\IpAttemptModel; use CodeIgniter\Controller; use App\Controllers\View\EmailController; use App\Models\LoginActivityModel; // Make sure this import is present +use App\Services\StaffDirectorySyncService; require_once APPPATH . 'Helpers/pbkdf2_helper.php'; @@ -29,6 +30,7 @@ class UserController extends BaseController protected $passwordResetModel; protected $loginActivityModel; protected $resetRequestModel; + protected StaffDirectorySyncService $staffDirectorySync; public function __construct() { @@ -48,6 +50,7 @@ class UserController extends BaseController $this->passwordResetModel = new PasswordResetModel(); $this->loginActivityModel = new LoginActivityModel(); $this->resetRequestModel = new PasswordResetRequestModel(); + $this->staffDirectorySync = service('staffDirectorySync'); } private function denyAccess(string $message) @@ -382,6 +385,8 @@ class UserController extends BaseController return redirect()->back()->withInput()->with('errors', $this->userRoleModel->errors()); } + $this->staffDirectorySync->syncUser((int) $userId); + return redirect()->to('/user'); } @@ -412,6 +417,7 @@ class UserController extends BaseController // Delete the user's roles from the user_roles table $this->userRoleModel->where('user_id', $id)->delete(); + $this->staffDirectorySync->syncUser((int) $id); return redirect()->to('/user'); } diff --git a/app/Models/TeacherClassModel.php b/app/Models/TeacherClassModel.php index 1abfce7..9d283c8 100644 --- a/app/Models/TeacherClassModel.php +++ b/app/Models/TeacherClassModel.php @@ -27,6 +27,8 @@ class TeacherClassModel extends Model protected $returnType = 'array'; protected $useSoftDeletes = false; protected $skipValidation = false; + protected $afterInsert = ['syncStaffDirectoryAfterWrite']; + protected $afterUpdate = ['syncStaffDirectoryAfterWrite']; /** Request-lifetime memo cache */ protected array $assignedCache = []; protected array $assignedBySectionCache = []; @@ -57,6 +59,43 @@ class TeacherClassModel extends Model ], ]; + protected function syncStaffDirectoryAfterWrite(array $data): array + { + try { + $userIds = []; + $teacherId = (int) ($data['data']['teacher_id'] ?? 0); + if ($teacherId > 0) { + $userIds[] = $teacherId; + } + + if ($userIds === [] && ! empty($data['id'])) { + $ids = is_array($data['id']) ? $data['id'] : [$data['id']]; + $ids = array_values(array_filter(array_map('intval', $ids), static fn (int $id): bool => $id > 0)); + if ($ids !== []) { + $rows = $this->db->table($this->table) + ->select('teacher_id') + ->whereIn($this->primaryKey, $ids) + ->get() + ->getResultArray(); + foreach ($rows as $row) { + $rowTeacherId = (int) ($row['teacher_id'] ?? 0); + if ($rowTeacherId > 0) { + $userIds[] = $rowTeacherId; + } + } + } + } + + foreach (array_unique($userIds) as $userId) { + service('staffDirectorySync')->syncUser((int) $userId); + } + } catch (\Throwable $e) { + log_message('error', 'TeacherClassModel staff directory sync failed: ' . $e->getMessage()); + } + + return $data; + } + public function getClassSectionIdByUserId($user_id) { $result = $this->where('teacher_id', $user_id)->first(); diff --git a/app/Models/UserRoleModel.php b/app/Models/UserRoleModel.php index 25afc35..a0c562a 100644 --- a/app/Models/UserRoleModel.php +++ b/app/Models/UserRoleModel.php @@ -23,6 +23,34 @@ class UserRoleModel extends Model protected $createdField = 'created_at'; protected $updatedField = 'updated_at'; protected $deletedField = 'deleted_at'; + protected $afterInsert = ['syncStaffDirectoryAfterWrite']; + protected $afterUpdate = ['syncStaffDirectoryAfterWrite']; + protected $afterDelete = ['syncStaffDirectoryAfterDelete']; + + protected function syncStaffDirectoryAfterWrite(array $data): array + { + try { + $userId = (int) ($data['data']['user_id'] ?? 0); + if ($userId > 0) { + service('staffDirectorySync')->syncUser($userId); + } + } catch (\Throwable $e) { + log_message('error', 'UserRoleModel staff directory sync failed: ' . $e->getMessage()); + } + + return $data; + } + + protected function syncStaffDirectoryAfterDelete(array $data): array + { + try { + service('staffDirectorySync')->syncAll(); + } catch (\Throwable $e) { + log_message('error', 'UserRoleModel staff directory delete sync failed: ' . $e->getMessage()); + } + + return $data; + } /** * ✅ Fetch all role names assigned to a specific user diff --git a/app/Services/SchoolYearManagementService.php b/app/Services/SchoolYearManagementService.php index b34c243..6200909 100644 --- a/app/Services/SchoolYearManagementService.php +++ b/app/Services/SchoolYearManagementService.php @@ -25,8 +25,14 @@ final class SchoolYearManagementService public function createDraft(array $payload, ?int $userId = null): int { + $nextDraft = $this->nextDraftDefaults(); + if ($nextDraft['name'] === null) { + throw new InvalidArgumentException('Create an initial school year before using automatic next-year draft creation.'); + } + + $payload['name'] = $nextDraft['name']; $payload = $this->metadataPayload($payload); - $payload['previous_school_year_id'] = $this->previousYearIdForDraft((string) $payload['name']); + $payload['previous_school_year_id'] = (int) ($nextDraft['previous_year']['id'] ?? 0) ?: $this->previousYearIdForDraft((string) $payload['name']); $payload['status'] = SchoolYearStatus::DRAFT; $payload['created_by'] = $userId; $payload['updated_by'] = $userId; @@ -48,6 +54,19 @@ final class SchoolYearManagementService return (int) $id; } + public function nextDraftDefaults(): array + { + $previousYear = $this->sourceYearForNextDraft(); + $name = $previousYear !== null + ? $this->nextSchoolYearName((string) ($previousYear['name'] ?? '')) + : null; + + return [ + 'name' => $name, + 'previous_year' => $previousYear, + ]; + } + public function updateMetadata(int $id, array $payload, ?int $userId = null): void { $year = $this->requireYear($id); @@ -321,6 +340,29 @@ final class SchoolYearManagementService return $latestYear !== null ? ((int) ($latestYear['id'] ?? 0) ?: null) : null; } + private function sourceYearForNextDraft(): ?array + { + $activeYear = $this->schoolYearModel->active(); + if ($activeYear !== null) { + return $activeYear; + } + + return $this->schoolYearModel + ->orderBy('name', 'DESC') + ->first(); + } + + private function nextSchoolYearName(string $name): ?string + { + if (! preg_match('/^(\d{4})-(\d{4})$/', $name, $matches)) { + return null; + } + + $start = (int) $matches[2]; + + return $start . '-' . ($start + 1); + } + private function nullableDate(mixed $value): ?string { $value = trim((string) $value); diff --git a/app/Services/StaffDirectorySyncService.php b/app/Services/StaffDirectorySyncService.php new file mode 100644 index 0000000..d470160 --- /dev/null +++ b/app/Services/StaffDirectorySyncService.php @@ -0,0 +1,232 @@ +db->table('users') + ->select('id') + ->get() + ->getResultArray(); + + foreach ($rows as $row) { + $userId = (int) ($row['id'] ?? 0); + if ($userId > 0) { + $this->syncUser($userId); + } + } + } + + public function syncUser(int $userId): void + { + if ($userId <= 0) { + return; + } + + $user = $this->userModel->find($userId); + if (! $user) { + return; + } + + $roles = $this->rolesForUser($userId); + $staffRoles = array_values(array_filter($roles, static function (array $role): bool { + return strtolower((string) ($role['name'] ?? '')) !== 'parent'; + })); + + $now = utc_now(); + $schoolYear = (string) ($this->configurationModel->getConfig('school_year') ?? ''); + $roleNames = array_values(array_unique(array_map( + static fn (array $role): string => strtolower((string) ($role['name'] ?? '')), + $roles + ))); + + $activeRole = ! empty($staffRoles) + ? strtolower((string) ($staffRoles[0]['name'] ?? '')) + : 'inactive'; + + $this->staffModel->upsert([ + 'user_id' => $userId, + 'firstname' => (string) ($user['firstname'] ?? ''), + 'lastname' => (string) ($user['lastname'] ?? ''), + 'email' => $this->generateUniqueStaffEmail( + (string) ($user['firstname'] ?? ''), + (string) ($user['lastname'] ?? ''), + $userId + ), + 'phone' => (string) ($user['cellphone'] ?? ''), + 'role_name' => implode(', ', $roleNames), + 'active_role' => $activeRole, + 'school_year' => $schoolYear, + 'updated_at' => $now, + ]); + } + + public function activeStaffForSchoolYear(string $schoolYear): array + { + $this->syncAll(); + + $staffRows = $this->db->table('users u') + ->select([ + 'u.id AS user_id', + 'u.school_id', + 'u.firstname', + 'u.lastname', + 'u.email AS personal_email', + 'u.cellphone', + 's.id AS staff_id', + 's.email AS work_email', + 's.active_role', + 's.role_name', + "GROUP_CONCAT(DISTINCT r.name ORDER BY COALESCE(r.priority, 999), r.name SEPARATOR ', ') AS roles", + ]) + ->join('user_roles ur', 'ur.user_id = u.id', 'inner') + ->join('roles r', 'r.id = ur.role_id', 'inner') + ->join('staff s', 's.user_id = u.id', 'left') + ->where("LOWER(r.name) != 'parent'", null, false) + ->where('COALESCE(r.is_active, 1) = 1', null, false); + + if ($this->hasField('user_roles', 'deleted_at')) { + $staffRows->where('ur.deleted_at', null); + } + + $rows = $staffRows + ->groupBy('u.id, u.school_id, u.firstname, u.lastname, u.email, u.cellphone, s.id, s.email, s.active_role, s.role_name') + ->orderBy('u.lastname', 'ASC') + ->orderBy('u.firstname', 'ASC') + ->get() + ->getResultArray(); + + $assignments = $this->assignmentsByTeacher($schoolYear); + $issuesCount = 0; + + foreach ($rows as &$row) { + $userId = (int) ($row['user_id'] ?? 0); + $activeRole = strtolower((string) ($row['active_role'] ?? '')); + $roleText = strtolower((string) ($row['roles'] ?? '')); + $isTeacher = in_array($activeRole, ['teacher', 'teacher_assistant'], true) + || str_contains($roleText, 'teacher') + || preg_match('/(^|, )ta($|,)/', $roleText); + + $labels = $assignments[$userId] ?? []; + if ($isTeacher) { + if ($labels !== []) { + $row['class_section'] = implode(', ', array_unique($labels)); + $row['verification_issue'] = false; + } else { + $row['class_section'] = 'No class assigned'; + $row['verification_issue'] = true; + $issuesCount++; + } + } else { + $row['class_section'] = '-'; + $row['verification_issue'] = false; + } + } + unset($row); + + return [ + 'staff' => $rows, + 'issues_count' => $issuesCount, + ]; + } + + private function rolesForUser(int $userId): array + { + $builder = $this->db->table('user_roles ur') + ->select('r.name, r.priority') + ->join('roles r', 'r.id = ur.role_id', 'inner') + ->where('ur.user_id', $userId) + ->where('COALESCE(r.is_active, 1) = 1', null, false) + ->orderBy('COALESCE(r.priority, 999)', 'ASC', false) + ->orderBy('r.name', 'ASC'); + + if ($this->hasField('user_roles', 'deleted_at')) { + $builder->where('ur.deleted_at', null); + } + + return $builder->get()->getResultArray(); + } + + private function assignmentsByTeacher(string $schoolYear): array + { + if ($schoolYear === '' || ! $this->db->tableExists('teacher_class')) { + return []; + } + + $rows = $this->db->table('teacher_class tc') + ->select('tc.teacher_id, tc.position, cs.class_section_name') + ->join('classSection cs', 'cs.class_section_id = tc.class_section_id', 'left') + ->where('tc.school_year', $schoolYear) + ->get() + ->getResultArray(); + + $assignments = []; + foreach ($rows as $row) { + $teacherId = (int) ($row['teacher_id'] ?? 0); + $name = trim((string) ($row['class_section_name'] ?? '')); + $position = strtolower((string) ($row['position'] ?? '')); + + if ($teacherId <= 0 || $name === '' || ! in_array($position, ['main', 'ta'], true)) { + continue; + } + + $assignments[$teacherId][] = $name . ' (' . $position . ')'; + } + + return $assignments; + } + + private function generateUniqueStaffEmail(string $firstname, string $lastname, int $userId): string + { + $first = strtolower((string) preg_replace('/[^a-z]/i', '', $firstname)); + $last = strtolower((string) preg_replace('/[^a-z]/i', '', $lastname)); + + if ($last === '') { + $last = 'user' . $userId; + } + + $base = $last; + $max = max(1, strlen($first)); + for ($i = 1; $i <= $max; $i++) { + $base = substr($first, 0, $i) . $last; + $email = $base . self::WORK_EMAIL_DOMAIN; + + $exists = $this->staffModel + ->where('email', $email) + ->where('user_id !=', $userId) + ->first(); + + if (! $exists) { + return $email; + } + } + + return $base . $userId . self::WORK_EMAIL_DOMAIN; + } + + private function hasField(string $table, string $field): bool + { + try { + return $this->db->tableExists($table) && $this->db->fieldExists($field, $table); + } catch (\Throwable) { + return false; + } + } +} diff --git a/app/Views/attendance/teacher_attendance_month.php b/app/Views/attendance/teacher_attendance_month.php index 4f1fee1..26f23af 100644 --- a/app/Views/attendance/teacher_attendance_month.php +++ b/app/Views/attendance/teacher_attendance_month.php @@ -40,19 +40,6 @@ $queryString = http_build_query(array_filter($queryParams, static function ($val