request->getPost('user_ids') ?? []; if (!is_array($userIds)) $userIds = [$userIds]; $userIds = array_values(array_filter(array_map(static function ($v) { $v = is_string($v) ? trim($v) : $v; return ($v !== '' && $v !== null) ? (int)$v : null; }, $userIds), static fn($v) => $v !== null)); $userIds = array_values(array_unique($userIds)); // 2) Consistent school_year $schoolYear = $this->request->getPost('school_year') ?? $this->request->getGet('school_year') ?? ($this->schoolYear ?? null); // 2a) Posted maps from the view $rolesMap = $this->request->getPost('roles') ?? []; $classesMap = $this->request->getPost('classes') ?? []; if (!is_array($rolesMap)) $rolesMap = []; if (!is_array($classesMap)) $classesMap = []; // 2b) Normalizer to mirror the view's formatting $formatRole = static function (?string $role): string { $role = (string)$role; $role = str_replace(['-', '_'], ' ', $role); $role = preg_replace('/\s+/', ' ', trim($role)); if ($role === '') return ''; $out = []; foreach (explode(' ', $role) as $w) { if ($w === '') continue; if (preg_match('/^[A-Za-z]{1,3}$/', $w)) { $out[] = strtoupper($w); } else { $out[] = ucfirst(strtolower($w)); } } return implode(' ', $out); }; // 3) PDF setup $pdf = new \FPDF('P', 'mm', 'A4'); $pdf->SetAutoPageBreak(false); // Layout: 2 cols - 5 rows = 10 per page $badgeW = 95; $badgeH = 67; $marginL = 14; $marginT = 6; $gutterX = 0; $gutterY = 0; $cols = 2; $rows = 4; $perPage = $cols * $rows; $pagesAdded = 0; $i = 0; $norm = static function (?string $s): string { $s = (string)$s; $s = str_replace(["\xC2\xA0", "\xA0"], ' ', $s); $s = preg_replace('/\s+/u', ' ', $s) ?? $s; $s = preg_replace('/[\x00-\x08\x0B-\x0C\x0E-\x1F\x7F]/u', '', $s) ?? $s; return trim($s); }; $resolveRole = static function (array $info) use ($norm): string { $candidates = [ 'role', 'active_role', 'role_name', function ($r) { if (!empty($r['roles'])) { $parts = array_filter(array_map('trim', explode(',', (string)$r['roles']))); return $parts[0] ?? ''; } return ''; }, 'job_title', 'title', 'position', 'staff_role', 'department_role', 'dept_role' ]; foreach ($candidates as $key) { if (is_callable($key)) { $v = $key($info); if ($norm($v) !== '') return $norm($v); } else { if (!empty($info[$key])) { $v = $norm((string)$info[$key]); if ($v !== '') return $v; } } } return 'STAFF'; }; $seen = []; foreach ($userIds as $uid) { $info = $this->getUserInfoById($uid, $schoolYear); if (!$info || !is_array($info)) continue; $userId = isset($info['user_id']) ? (int)$info['user_id'] : (isset($info['id']) ? (int)$info['id'] : (isset($info['users.id']) ? (int)$info['users.id'] : $uid)); $name = $norm($info['name'] ?? (($info['firstname'] ?? '') . ' ' . ($info['lastname'] ?? ''))); // Prefer posted formatted role; else fallback and format server-side $postedRole = $rolesMap[$userId] ?? null; $roleResolved = $postedRole !== null ? $postedRole : $resolveRole($info); $roleResolved = $formatRole((string)$roleResolved); $info['role_resolved'] = $roleResolved; // Prefer posted class name if provided (keeps what the user saw) if (!empty($classesMap[$userId])) { $info['class_section_name'] = (string)$classesMap[$userId]; } $class = $norm($info['class_section_name'] ?? ''); $badgeKey = strtolower(trim($userId . '|' . $name . '|' . $roleResolved . '|' . $class)); if (isset($seen[$badgeKey])) continue; $seen[$badgeKey] = true; if (($i % $perPage) === 0) { $pdf->AddPage(); $pagesAdded++; } $indexOnPage = $i % $perPage; $row = intdiv($indexOnPage, $cols); $col = $indexOnPage % $cols; $x = $marginL + $col * ($badgeW + $gutterX); $y = $marginT + $row * ($badgeH + $gutterY); // drawBadgeInCell expects 'role_resolved' and (optionally) 'class_section_name' $this->drawBadgeInCell($pdf, $info, $x, $y, $badgeW, $badgeH); $i++; } if ($pagesAdded === 0) { $pdf->AddPage(); $pdf->SetFont('Arial', '', 10); $pdf->SetXY(10, 20); $pdf->MultiCell(0, 6, "No valid staff selected or data not found.", 0, 'L'); } if (ob_get_length()) { ob_end_clean(); } $pdfString = $pdf->Output('S'); // 4) Track prints (best-effort; swallow errors if table missing) try { $actorId = (int) (session()->get('user_id') ?? 0) ?: null; $this->badgePrintLogModel->logPrints( $userIds, $actorId, is_string($schoolYear) ? $schoolYear : null, is_array($rolesMap) ? $rolesMap : [], is_array($classesMap) ? $classesMap : [], 1 ); } catch (\Throwable $e) { log_message('error', 'Failed to log badge prints: ' . $e->getMessage()); } return $this->response ->setHeader('Content-Type', 'application/pdf') ->setHeader('Content-Disposition', 'inline; filename="Staff_Badges.pdf"') ->setBody($pdfString); } /** * API: Return print status for given users (count + last print time). * GET: user_ids[] or CSV in user_ids, optional school_year * Response: { ok: true, data: { : { count, last_printed_at, last_printed_by } } } */ public function badgePrintStatus() { $ids = $this->request->getGet('user_ids'); if (is_string($ids)) { $ids = array_filter(array_map('trim', explode(',', $ids)), 'strlen'); } if (!is_array($ids)) $ids = $ids ? [$ids] : []; $ids = array_values(array_unique(array_map(static fn($v) => (int)$v, $ids))); $schoolYear = $this->request->getGet('school_year') ?? $this->schoolYear; try { $status = $this->badgePrintLogModel->getStatus($ids, is_string($schoolYear) ? $schoolYear : null); } catch (\Throwable $e) { return $this->response->setJSON(['ok' => false, 'error' => 'Failed to query status']); } return $this->response->setJSON([ 'ok' => true, 'data' => $status, 'csrf_token' => csrf_token(), 'csrf_hash' => csrf_hash(), ]); } /** * API: Log a set of prints explicitly (optional; PDF endpoint already logs). * POST: user_ids[], optional school_year, roles[ID], classes[ID] */ public function logBadgePrint() { $ids = $this->request->getPost('user_ids') ?? []; if (!is_array($ids)) $ids = $ids ? [$ids] : []; $ids = array_values(array_unique(array_map(static fn($v) => (int)$v, $ids))); $roles = $this->request->getPost('roles') ?? []; $class = $this->request->getPost('classes') ?? []; if (!is_array($roles)) $roles = []; if (!is_array($class)) $class = []; $schoolYear = $this->request->getPost('school_year') ?? $this->schoolYear; $actorId = (int) (session()->get('user_id') ?? 0) ?: null; try { $cnt = $this->badgePrintLogModel->logPrints( $ids, $actorId, is_string($schoolYear) ? $schoolYear : null, $roles, $class, 1 ); return $this->response->setJSON([ 'ok' => true, 'inserted' => $cnt, 'csrf_token' => csrf_token(), 'csrf_hash' => csrf_hash(), ]); } catch (\Throwable $e) { return $this->response->setJSON(['ok' => false, 'error' => $e->getMessage()]); } } protected function drawBadgeInCell(\FPDF $pdf, array $data, float $x, float $y, float $w, float $h): void { // Background & border $pdf->SetFillColor(255, 255, 255); $pdf->Rect($x, $y, $w, $h, 'F'); $pdf->SetDrawColor(200, 200, 200); $pdf->Rect($x, $y, $w, $h); // Logos $logoSize = 6; if (!empty($data['school_logo']) && file_exists($data['school_logo'])) { $pdf->Image($data['school_logo'], $x + $w - 6 - $logoSize, $y + 4, $logoSize + 3, $logoSize - 1); } if (!empty($data['isgl_logo']) && file_exists($data['isgl_logo'])) { $pdf->Image($data['isgl_logo'], $x + 4, $y + 4, $logoSize + 3, $logoSize - 1); } // Helpers $padX = 4; $cursorY = $y + 4 + $logoSize + 2; $norm = static function (?string $s): string { $s = (string)$s; $s = str_replace(["\xC2\xA0", "\xA0"], ' ', $s); $s = preg_replace('/\s+/u', ' ', $s) ?? $s; $s = preg_replace('/[\x00-\x08\x0B-\x0C\x0E-\x1F\x7F]/u', '', $s) ?? $s; return trim($s); }; $toPdf = static function (string $s): string { if (function_exists('iconv')) { $o = @iconv('UTF-8', 'ISO-8859-1//TRANSLIT', $s); if ($o !== false && $o !== '') return $o; } if (function_exists('mb_convert_encoding')) { $o = @mb_convert_encoding($s, 'ISO-8859-1', 'UTF-8'); if ($o !== '') return $o; } $o = @utf8_decode($s); return $o !== '' ? $o : 'STAFF'; }; $fitText = static function (\FPDF $pdf, string $text, int $startSize, int $minSize, float $maxWidth): int { $size = $startSize; while ($size >= $minSize) { $pdf->SetFontSize($size); if ($pdf->GetStringWidth($text) <= $maxWidth) return $size; $size--; } return $minSize; }; // === Role formatter (fixes "OF" -> "Of", "HEAD" -> "Head"; keeps TA/PTA/HR/KG/IT/DEPT in ALL-CAPS) === $formatRole = static function (string $role): string { // Normalize separators & spaces $role = str_replace(['-', '_'], ' ', $role); $role = preg_replace('/\s+/u', ' ', trim($role)); if ($role === '') return ''; // Special-casing map (case-insensitive keys) // - "of" must be lower-case // - "head" should be Title-case $special = [ 'of' => 'of', 'head' => 'Head', ]; $tokens = explode(' ', $role); $out = []; foreach ($tokens as $tok) { if ($tok === '') continue; // Strip leading/trailing NON-letters for the decision; reattach afterwards $core = preg_replace('/^\P{L}+|\P{L}+$/u', '', $tok); $start = strpos($tok, $core); if ($start === false) { // no letter content $out[] = ucfirst(mb_strtolower($tok, 'UTF-8')); continue; } $pre = substr($tok, 0, $start); $suf = substr($tok, $start + strlen($core)); $lw = mb_strtolower($core, 'UTF-8'); if (isset($special[$lw])) { // Use the exact desired casing from the map $coreFmt = $special[$lw]; // "of" -> "of", "head" -> "Head" } elseif (preg_match('/^\p{L}{1,4}$/u', $core)) { // 1-4 letters -> ALL CAPS (TA, PTA, HR, KG, IT, DEPT) $coreFmt = mb_strtoupper($core, 'UTF-8'); } else { // Title-case longer words $coreFmt = preg_replace_callback( '/\p{L}+/u', static fn($m) => mb_strtoupper(mb_substr($m[0], 0, 1, 'UTF-8'), 'UTF-8') . mb_strtolower(mb_substr($m[0], 1, null, 'UTF-8'), 'UTF-8'), mb_strtolower($core, 'UTF-8') ); } $out[] = $pre . $coreFmt . $suf; } return implode(' ', $out); }; // Class label formatter (KG stays KG; Youth title-cased; otherwise pass through) $formatClass = static function (string $class): string { $c = trim($class); if ($c === '') return ''; $lc = strtolower($c); if ($lc === 'kg' || $lc === 'kindergarten') return 'KG'; if ($lc === 'youth') return 'Youth'; return $c; // e.g., "1-A", "3", "HS-2" }; // School name $schoolName = strtoupper($norm($data['school_name'] ?? 'AL RAHMA SUNDAY SCHOOL')); $pdf->SetFont('Arial', '', 16); $pdf->SetXY($x + $padX, $cursorY); $pdf->MultiCell($w - 2 * $padX, 5, $toPdf($schoolName), 0, 'C'); // Name $pdf->Ln(9); $pdf->SetFont('Arial', 'B', 14); $pdf->SetX($x + $padX); $name = strtoupper($norm($data['name'] ?? (($data['firstname'] ?? '') . ' ' . ($data['lastname'] ?? 'STAFF')))); $pdf->Cell($w - 2 * $padX, 6, $toPdf($name), 0, 1, 'C'); // Role (prefer preformatted; then fix with formatter to guarantee "Of"/"Head" etc.) $roleResolved = $norm((string)($data['role_resolved'] ?? '')); if ($roleResolved === '') { // fallback: find first available role-like field $candidates = [ 'role', 'active_role', 'role_name', function ($r) { if (!empty($r['roles'])) { $parts = array_filter(array_map('trim', explode(',', (string)$r['roles']))); return $parts[0] ?? ''; } return ''; }, 'job_title', 'title', 'position', 'staff_role', 'department_role', 'dept_role' ]; foreach ($candidates as $key) { $v = ''; if (is_callable($key)) { $v = (string)$key($data); } elseif (!empty($data[$key])) { $v = (string)$data[$key]; } $v = $norm($v); if ($v !== '') { $roleResolved = $v; break; } } } // Final fix to ensure exceptions and acronyms are correct even if upstream passed "OF" if ($roleResolved !== '') { $roleResolved = $formatRole($roleResolved); } // Class $classRaw = $norm($data['class_section_name'] ?? ''); $class = $formatClass($classRaw); // Teacher-ish detection (lowercased source; do NOT change printed casing) $detectSrc = strtolower($norm( ($data['roles_raw'] ?? '') !== '' ? $data['roles_raw'] : (($data['role_name_raw'] ?? '') !== '' ? $data['role_name_raw'] : $roleResolved) )); $isTeacherish = (strpos($detectSrc, 'teacher') !== false) || preg_match('/\bta\b/', $detectSrc); // Compose final display (no re-casing beyond $formatRole) if ($isTeacherish && $class !== '') { if (strtolower($class) === 'youth') { $display = 'Youth ' . $roleResolved; } elseif ($class === 'KG') { $display = 'KG ' . $roleResolved; } else { $display = 'Grade ' . $class . ' ' . $roleResolved; } } elseif ($roleResolved !== '') { $display = $roleResolved; } elseif ($class !== '') { $display = $class; } else { $display = 'STAFF'; } // Print role/class $pdf->Ln(6); $pdf->SetFont('Arial', '', 14); $displayOut = $toPdf($display); $maxTextWidth = $w - 2 * $padX; $best = $fitText($pdf, $displayOut, 11, 7, $maxTextWidth); $pdf->SetFont('Arial', '', $best + 4); if ($pdf->GetStringWidth($displayOut) <= $maxTextWidth) { $pdf->SetX($x + $padX); $pdf->Cell($maxTextWidth, 5, $displayOut, 0, 1, 'C'); } else { $pdf->SetX($x + $padX); $pdf->MultiCell($maxTextWidth, 5, $displayOut, 0, 'C'); } // Footer: year $footerYear = $norm((string)($this->schoolYear ?? ($data['school_year'] ?? ''))); if ($footerYear !== '') { $pdf->SetFont('Arial', 'I', 14); $pdf->SetXY($x + $padX, $y + $h - 9); $pdf->Cell($w - 2 * $padX, 4, $toPdf($footerYear), 0, 0, 'C'); } } // ---- Helper: get staff rows (no joins) ---- private function fetchStaffList(?string $schoolYear, array $selectedUserIds = []): array { // $this->userModel should be injected/constructed already return $this->userModel->getNoParentUsersWithRole($schoolYear, $selectedUserIds); } // ---- Helper: is this person teacher-ish? (role_name can be CSV) ---- private static function isTeacherish(string $roleCsv): bool { $roles = array_filter(array_map('trim', explode(',', strtolower($roleCsv)))); foreach ($roles as $r) { if ($r === 'teacher' || $r === 'teacher_assistant' || $r === 'teacher assistant') { return true; } } return false; } // ---- Helper: return ONE latest assignment (pref Teacher, else Assistant) and its class name ---- private function getLatestClassForUser(int $userId, ?string $schoolYear = null): ?array { // Build a single query that considers teacher or assistant rows, $qb = $this->db->table('teacher_class tc') ->select('tc.class_section_id, cs.class_section_name') ->join('classSection cs', 'cs.class_section_id = tc.class_section_id', 'left') ->where('tc.teacher_id', $userId); if (!empty($schoolYear)) { $qb->where('tc.school_year', $schoolYear); } $qb->orderBy('FIELD(tc.position, "main", "ta")', '', false) ->orderBy('IFNULL(tc.updated_at,"0000-00-00 00:00:00") DESC', '', false) ->limit(1); if (!empty($schoolYear)) { $qb->where('tc.school_year', $schoolYear); } // Prefer a TEACHER row if one exists, then the latest update. // MySQL treats boolean expressions as 0/1, so we can order by it. $qb->orderBy('(tc.teacher_id = ' . $this->db->escape($userId) . ') DESC', '', false) ->orderBy('IFNULL(tc.updated_at,"0000-00-00 00:00:00") DESC, tc.id DESC', '', false) ->limit(1); $row = $qb->get()->getRowArray(); if (!$row || empty($row['class_section_id'])) { return null; } return [ 'class_section_id' => (int) $row['class_section_id'], 'class_section_name' => $row['class_section_name'] ?? null, ]; } // ---- Controller: badgeForm (simple + deterministic) ---- public function badgeForm() { $request = $this->request; $schoolYear = $this->schoolYear; if ($this->schoolYear === null || $schoolYear === '') { $schoolYear = $this->schoolYear ?? null; } // --- Role formatting helpers --- $formatRole = static function (string $role): string { // Normalize separators & spaces $role = str_replace(['-', '_'], ' ', $role); $role = preg_replace('/\s+/u', ' ', trim($role)); if ($role === '') return ''; // Special-casing map (case-insensitive keys) // - "of" must be lower-case // - "head" should be Title-case $special = [ 'of' => 'of', 'head' => 'Head', ]; $tokens = explode(' ', $role); $out = []; foreach ($tokens as $tok) { if ($tok === '') continue; // Strip leading/trailing NON-letters for the decision; reattach afterwards $core = preg_replace('/^\P{L}+|\P{L}+$/u', '', $tok); $start = strpos($tok, $core); if ($start === false) { // no letter content $out[] = ucfirst(mb_strtolower($tok, 'UTF-8')); continue; } $pre = substr($tok, 0, $start); $suf = substr($tok, $start + strlen($core)); $lw = mb_strtolower($core, 'UTF-8'); if (isset($special[$lw])) { // Use the exact desired casing from the map $coreFmt = $special[$lw]; // "of" -> "of", "head" -> "Head" } elseif (preg_match('/^\p{L}{1,4}$/u', $core)) { // 1-4 letters -> ALL CAPS (TA, PTA, HR, KG, IT, DEPT) $coreFmt = mb_strtoupper($core, 'UTF-8'); } else { // Title-case longer words $coreFmt = preg_replace_callback( '/\p{L}+/u', static fn($m) => mb_strtoupper(mb_substr($m[0], 0, 1, 'UTF-8'), 'UTF-8') . mb_strtolower(mb_substr($m[0], 1, null, 'UTF-8'), 'UTF-8'), mb_strtolower($core, 'UTF-8') ); } $out[] = $pre . $coreFmt . $suf; } return implode(' ', $out); }; $formatRolesCsv = static function ($csv) use ($formatRole): string { if ($csv === null) return ''; $parts = array_filter(array_map('trim', explode(',', (string)$csv)), 'strlen'); if (empty($parts)) return ''; $parts = array_map($formatRole, $parts); return implode(', ', $parts); }; // --- 2) Selected user IDs: accept array, single value, or CSV --- $selectedUsers = $request->getGet('user_ids'); if (is_string($selectedUsers)) { // Handle CSV like "12,34,56" $selectedUsers = array_filter(array_map('trim', explode(',', $selectedUsers)), 'strlen'); } if (!is_array($selectedUsers)) { $selectedUsers = $selectedUsers ? [$selectedUsers] : []; } // Normalize to unique ints $selectedUserIds = array_values(array_unique(array_map(static function ($v) { return (int) $v; }, $selectedUsers))); // --- 3) Base staff rows (non-parent users) --- // fetchStaffList() should call your model method getNoParentUsersWithRole($schoolYear, $selectedUserIds) $users = $this->fetchStaffList($schoolYear, $selectedUserIds); // Normalize keys so the view logic is consistent: // Expect at least: id (or users.id), firstname, lastname, email, roles (CSV of non-parent roles) foreach ($users as &$u) { // Ensure user_id exists if (!isset($u['user_id'])) { if (isset($u['users.id'])) { $u['user_id'] = (int) $u['users.id']; } elseif (isset($u['id'])) { $u['user_id'] = (int) $u['id']; } elseif (isset($u['users_id'])) { $u['user_id'] = (int) $u['users_id']; } } // Ensure roles string exists (model may already provide it as 'roles') $rolesStr = $u['roles'] ?? ''; if ($rolesStr === '' && isset($u['role_name'])) { // fallback if model returned single role $rolesStr = (string) $u['role_name']; } // Keep raw + add formatted CSV $u['roles_raw'] = $rolesStr; $u['roles'] = $formatRolesCsv($rolesStr); // Provide a role_name for legacy view code (pick a representative non-parent role) if (empty($u['role_name'])) { $firstRole = ''; if ($rolesStr !== '') { $parts = array_filter(array_map('trim', explode(',', $rolesStr))); $firstRole = $parts[0] ?? ''; } $u['role_name'] = $firstRole; } // Keep raw + add formatted single role name $u['role_name_raw'] = $u['role_name']; $u['role_name'] = $formatRole((string)$u['role_name']); // Defaults for class assignment (only filled for teacherish) $u['class_section_id'] = $u['class_section_id'] ?? null; $u['class_section_name'] = $u['class_section_name'] ?? null; } unset($u); // --- 4) For teachers/assistants, fetch latest assignment in this year --- foreach ($users as &$u) { // Use RAW roles for detection to avoid any formatting side-effects $rolesDetect = strtolower($u['roles_raw'] ?? ($u['role_name_raw'] ?? '')); $isTeacherish = (strpos($rolesDetect, 'teacher') !== false) || preg_match('/\bta\b/', $rolesDetect); if ($isTeacherish && !empty($u['user_id'])) { $assign = $this->getLatestClassForUser((int) $u['user_id'], $schoolYear); if ($assign) { $u['class_section_id'] = $assign['class_section_id'] ?? null; $u['class_section_name'] = $assign['class_section_name'] ?? null; } } } unset($u); // --- 5) Build list of available school years (robust) --- $db = \Config\Database::connect(); $schoolYears = []; // Prefer teacher_class if available try { if (method_exists($db, 'tableExists') ? $db->tableExists('teacher_class') : true) { $q1 = $db->table('teacher_class') ->select('DISTINCT school_year', false) ->where('school_year IS NOT NULL', null, false) ->orderBy('school_year', 'DESC') ->get() ->getResultArray(); $schoolYears = array_column($q1, 'school_year'); } } catch (\Throwable $e) { // ignore and try fallback } // Fallback to user_roles if teacher_class didn't produce anything if (empty($schoolYears)) { try { if (method_exists($db, 'tableExists') ? $db->tableExists('user_roles') : true) { $fields = $db->getFieldNames('user_roles'); if (in_array('school_year', $fields, true)) { $q2 = $db->table('user_roles') ->select('DISTINCT school_year', false) ->where('school_year IS NOT NULL', null, false) ->orderBy('school_year', 'DESC') ->get() ->getResultArray(); $schoolYears = array_column($q2, 'school_year'); } } } catch (\Throwable $e) { // swallow; leave $schoolYears empty } } // --- 6) Active role tab (defensive default for the view) --- $rolesTabs = [ 'teacher' => 'Teachers', 'ta' => 'Teacher Assistants', 'admin' => 'Admins', 'staff' => 'Staff', ]; $requestedRole = $request->getGet('active_role') ?? $request->getPost('active_role') ?? null; $activeRole = array_key_exists((string)$requestedRole, $rolesTabs) ? (string)$requestedRole : 'teacher'; // --- 7) Pack data for the view --- $data = [ 'users' => $users, 'schoolYears' => $schoolYears, 'selectedYear' => $schoolYear, 'selectedUserIds' => $selectedUserIds, 'rolesTabs' => $rolesTabs, 'active_role' => $activeRole, // prevents "Undefined array key 'active_role'" ]; return view('printables_reports/badge_form', $data); } protected function getUserInfoById($id, $schoolYear = null) { // ---- A) Resolve the user row $u = $this->db->table('users') ->select('id AS user_id, firstname, lastname') ->where('id', $id) ->limit(1) ->get()->getRowArray(); if (!$u) { return null; } $userId = (int) $u['user_id']; $fullname = trim(($u['firstname'] ?? '') . ' ' . ($u['lastname'] ?? '')); // ---- B) Get roles (non-parent) for this user $rolesRows = $this->db->table('user_roles ur') ->select('r.id AS role_id, r.name AS role_name') ->join('roles r', 'r.id = ur.role_id', 'inner') ->where('ur.user_id', $userId) ->get()->getResultArray(); $allRoles = []; foreach ($rolesRows as $rr) { $name = trim((string)($rr['role_name'] ?? '')); if ($name !== '' && strtolower($name) !== 'parent') { // normalize to "Ucwords" $allRoles[] = ucwords(strtolower($name)); } } $allRoles = array_values(array_unique($allRoles)); $rolesCsv = $allRoles ? implode(', ', $allRoles) : ''; // ---- C) Latest assignment from teacher_class $tc = $this->db->table('teacher_class') ->select('class_section_id, updated_at, id, position, school_year') ->where('teacher_id', $userId); if (!empty($schoolYear)) { $tc->where('school_year', $schoolYear); } $assignment = $tc->orderBy('IFNULL(updated_at, "1970-01-01 00:00:00") DESC, id DESC', '', false) ->limit(1) ->get()->getRowArray(); $classId = $assignment['class_section_id'] ?? null; $position = strtolower(trim((string)($assignment['position'] ?? ''))); $className = null; if (!empty($classId)) { $className = $this->resolveClassName($this->db, $classId); } // ---- D) Decide primary role label $roleLabel = ''; if ($position === 'ta' || $position === 'teacher_assistant' || $position === 'assistant') { $roleLabel = 'Teacher Assistant'; } elseif ($position === 'teacher') { $roleLabel = 'Teacher'; } elseif (!empty($allRoles)) { // Use first role from normalized list $roleLabel = $allRoles[0]; } else { $roleLabel = 'Staff'; } // ---- E) Logos $schoolLogo = $this->firstExisting([ FCPATH . 'assets/images/school_logo.png', FCPATH . 'assets/images/logo.png', ]); $isglLogo = $this->firstExisting([ FCPATH . 'assets/images/isgl_logo.png', FCPATH . 'assets/images/isgl.png', ]); // ---- F) Build job title / display title $jobTitle = ''; if (!empty($roleLabel) && !empty($className)) { $jobTitle = "{$roleLabel} - {$className}"; } elseif (!empty($roleLabel)) { $jobTitle = $roleLabel; } elseif (!empty($className)) { $jobTitle = $className; } return [ 'name' => $fullname !== '' ? $fullname : 'STAFF', 'role' => $roleLabel, // primary label 'roles' => $rolesCsv, // all roles (comma-separated) 'class_section_id' => $classId ? (int)$classId : null, 'class_section_name' => $className, 'job_title' => $jobTitle, 'school_name' => 'Al Rahma Sunday School', 'school_id' => $userId, 'school_logo' => $schoolLogo, 'isgl_logo' => $isglLogo, 'school_year' => $assignment['school_year'] ?? ($schoolYear ?? null), ]; } }