normalizeIds($userIds); $pdf = new FPDF('P', 'mm', 'A4'); $pdf->SetAutoPageBreak(false); $badgeW = 95; $badgeH = 67; $marginL = 14; $marginT = 6; $gutterX = 0; $gutterY = 0; $cols = 2; $rows = 4; $perPage = $cols * $rows; $pagesAdded = 0; $i = 0; $seen = []; foreach ($userIds as $uid) { $info = $this->getUserInfoById($uid, $schoolYear); if (! $info || ! is_array($info)) { continue; } $userId = (int) ($info['user_id'] ?? $uid); $name = $this->norm($info['name'] ?? ''); $postedRole = $rolesMap[$userId] ?? null; $roleResolved = $postedRole !== null ? $postedRole : $this->resolveRole($info); $roleResolved = $this->formatRole($roleResolved); $info['role_resolved'] = $roleResolved; if (! empty($classesMap[$userId])) { $info['class_section_name'] = (string) $classesMap[$userId]; } $class = $this->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); $this->drawBadgeInCell($pdf, $info, $x, $y, $badgeW, $badgeH, $schoolYear); $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'); } $pdfString = $pdf->Output('S'); try { $this->badgePrintLogModel->logPrints( $userIds, $actorId, $schoolYear, is_array($rolesMap) ? $rolesMap : [], is_array($classesMap) ? $classesMap : [], 1 ); } catch (\Throwable $e) { Log::error('Failed to log badge prints: '.$e->getMessage()); } return response($pdfString, 200, [ 'Content-Type' => 'application/pdf', 'Content-Disposition' => 'inline; filename="Staff_Badges.pdf"', ]); } public function getBadgePrintStatus(array $ids, ?string $schoolYear): array { $ids = $this->normalizeIds($ids); return $this->badgePrintLogModel->getStatus($ids, $schoolYear); } public function logBadgePrint( array $ids, ?string $schoolYear, array $roles = [], array $classes = [], ?int $actorId = null ): int { $ids = $this->normalizeIds($ids); return $this->badgePrintLogModel->logPrints( $ids, $actorId, $schoolYear, is_array($roles) ? $roles : [], is_array($classes) ? $classes : [], 1 ); } public function getBadgeFormData(?string $schoolYear, array $selectedUserIds = [], ?string $activeRole = null): array { $selectedUserIds = $this->normalizeIds($selectedUserIds); $users = $this->fetchStaffList($schoolYear, $selectedUserIds); foreach ($users as &$u) { if (! isset($u['user_id'])) { if (isset($u['id'])) { $u['user_id'] = (int) $u['id']; } } $rolesStr = $u['roles'] ?? ''; if ($rolesStr === '' && isset($u['role_name'])) { $rolesStr = (string) $u['role_name']; } $u['roles_raw'] = $rolesStr; $u['roles'] = $this->formatRolesCsv($rolesStr); if (empty($u['role_name'])) { $parts = array_filter(array_map('trim', explode(',', $rolesStr))); $u['role_name'] = $parts[0] ?? ''; } $u['role_name_raw'] = $u['role_name']; $u['role_name'] = $this->formatRole((string) $u['role_name']); $u['class_section_id'] = $u['class_section_id'] ?? null; $u['class_section_name'] = $u['class_section_name'] ?? null; } unset($u); foreach ($users as &$u) { $rolesDetect = strtolower($u['roles_raw'] ?? ($u['role_name_raw'] ?? '')); $isTeacherish = str_contains($rolesDetect, 'teacher') || 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); $schoolYears = $this->getAvailableSchoolYears(); $rolesTabs = [ 'teacher' => 'Teachers', 'ta' => 'Teacher Assistants', 'admin' => 'Admins', 'staff' => 'Staff', ]; $activeRole = array_key_exists((string) $activeRole, $rolesTabs) ? (string) $activeRole : 'teacher'; return [ 'users' => $users, 'schoolYears' => $schoolYears, 'selectedYear' => $schoolYear, 'selectedUserIds' => $selectedUserIds, 'rolesTabs' => $rolesTabs, 'active_role' => $activeRole, ]; } protected function fetchStaffList(?string $schoolYear, array $selectedUserIds = []): array { $query = DB::table('users') ->leftJoin('user_roles', 'user_roles.user_id', '=', 'users.id') ->leftJoin('roles', 'roles.id', '=', 'user_roles.role_id') ->select([ 'users.id', 'users.firstname', 'users.lastname', DB::raw("GROUP_CONCAT(DISTINCT CASE WHEN LOWER(roles.name) != 'parent' THEN roles.name END ORDER BY roles.name SEPARATOR ', ') as roles"), ]) ->groupBy('users.id', 'users.firstname', 'users.lastname'); if (! empty($selectedUserIds)) { $query->whereIn('users.id', $selectedUserIds); } return array_map(static fn ($row) => (array) $row, $query->get()->all()); } protected function getLatestClassForUser(int $userId, ?string $schoolYear = null): ?array { $query = DB::table('teacher_class as tc') ->leftJoin('classSection as cs', 'cs.class_section_id', '=', 'tc.class_section_id') ->select([ 'tc.class_section_id', 'cs.class_section_name', ]) ->where('tc.teacher_id', $userId); if (! empty($schoolYear)) { $query->where('tc.school_year', $schoolYear); } $row = $query ->orderByRaw("FIELD(tc.position, 'main', 'ta')") ->orderByRaw('IFNULL(tc.updated_at,"0000-00-00 00:00:00") DESC') ->orderByDesc('tc.id') ->first(); 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, ]; } public function getUserInfoById(int $id, ?string $schoolYear = null): ?array { $u = DB::table('users') ->select(['id as user_id', 'firstname', 'lastname']) ->where('id', $id) ->first(); if (! $u) { return null; } $userId = (int) $u->user_id; $fullname = trim(($u->firstname ?? '').' '.($u->lastname ?? '')); $rolesRows = DB::table('user_roles as ur') ->join('roles as r', 'r.id', '=', 'ur.role_id') ->select(['r.id as role_id', 'r.name as role_name']) ->where('ur.user_id', $userId) ->get(); $allRoles = []; foreach ($rolesRows as $rr) { $name = trim((string) ($rr->role_name ?? '')); if ($name !== '' && strtolower($name) !== 'parent') { $allRoles[] = ucwords(strtolower($name)); } } $allRoles = array_values(array_unique($allRoles)); $rolesCsv = $allRoles ? implode(', ', $allRoles) : ''; $tcQuery = DB::table('teacher_class') ->select(['class_section_id', 'updated_at', 'id', 'position', 'school_year']) ->where('teacher_id', $userId); if (! empty($schoolYear)) { $tcQuery->where('school_year', $schoolYear); } $assignment = $tcQuery ->orderByRaw('IFNULL(updated_at, "1970-01-01 00:00:00") DESC, id DESC') ->first(); $classId = $assignment->class_section_id ?? null; $position = strtolower(trim((string) ($assignment->position ?? ''))); $className = null; if (! empty($classId)) { $className = $this->resolveClassName((int) $classId); } if (in_array($position, ['ta', 'teacher_assistant', 'assistant'], true)) { $roleLabel = 'Teacher Assistant'; } elseif ($position === 'teacher' || $position === 'main') { $roleLabel = 'Teacher'; } elseif (! empty($allRoles)) { $roleLabel = $allRoles[0]; } else { $roleLabel = 'Staff'; } $schoolLogo = $this->firstExisting([ public_path('assets/images/school_logo.png'), public_path('assets/images/logo.png'), ]); $isglLogo = $this->firstExisting([ public_path('assets/images/isgl_logo.png'), public_path('assets/images/isgl.png'), ]); $jobTitle = ''; if (! empty($roleLabel) && ! empty($className)) { $jobTitle = "{$roleLabel} - {$className}"; } elseif (! empty($roleLabel)) { $jobTitle = $roleLabel; } elseif (! empty($className)) { $jobTitle = $className; } return [ 'user_id' => $userId, 'name' => $fullname !== '' ? $fullname : 'STAFF', 'role' => $roleLabel, 'roles' => $rolesCsv, '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, ]; } protected function drawBadgeInCell(FPDF $pdf, array $data, float $x, float $y, float $w, float $h, ?string $schoolYear): void { $pdf->SetFillColor(255, 255, 255); $pdf->Rect($x, $y, $w, $h, 'F'); $pdf->SetDrawColor(200, 200, 200); $pdf->Rect($x, $y, $w, $h); $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); } $padX = 4; $cursorY = $y + 4 + $logoSize + 2; $schoolName = strtoupper($this->norm($data['school_name'] ?? 'AL RAHMA SUNDAY SCHOOL')); $pdf->SetFont('Arial', '', 16); $pdf->SetXY($x + $padX, $cursorY); $pdf->MultiCell($w - 2 * $padX, 5, $this->toPdf($schoolName), 0, 'C'); $pdf->Ln(9); $pdf->SetFont('Arial', 'B', 14); $pdf->SetX($x + $padX); $name = strtoupper($this->norm($data['name'] ?? 'STAFF')); $pdf->Cell($w - 2 * $padX, 6, $this->toPdf($name), 0, 1, 'C'); $roleResolved = $this->norm((string) ($data['role_resolved'] ?? '')); if ($roleResolved === '') { $roleResolved = $this->resolveRole($data); } if ($roleResolved !== '') { $roleResolved = $this->formatRole($roleResolved); } $classRaw = $this->norm($data['class_section_name'] ?? ''); $class = $this->formatClass($classRaw); $detectSrc = strtolower($this->norm( ($data['roles_raw'] ?? '') !== '' ? $data['roles_raw'] : (($data['role_name_raw'] ?? '') !== '' ? $data['role_name_raw'] : $roleResolved) )); $isTeacherish = str_contains($detectSrc, 'teacher') || preg_match('/\bta\b/', $detectSrc); 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'; } $pdf->Ln(6); $pdf->SetFont('Arial', '', 14); $displayOut = $this->toPdf($display); $maxTextWidth = $w - 2 * $padX; $best = $this->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'); } $footerYear = $this->norm((string) ($schoolYear ?? ($data['school_year'] ?? ''))); if ($footerYear !== '') { $pdf->SetFont('Arial', 'I', 14); $pdf->SetXY($x + $padX, $y + $h - 9); $pdf->Cell($w - 2 * $padX, 4, $this->toPdf($footerYear), 0, 0, 'C'); } } protected function resolveRole(array $info): string { $candidates = [ 'role', 'active_role', 'role_name', fn ($r) => ! empty($r['roles']) ? (array_filter(array_map('trim', explode(',', (string) $r['roles'])))[0] ?? '') : '', 'job_title', 'title', 'position', 'staff_role', 'department_role', 'dept_role', ]; foreach ($candidates as $key) { if (is_callable($key)) { $v = $key($info); if ($this->norm($v) !== '') { return $this->norm($v); } } else { if (! empty($info[$key])) { $v = $this->norm((string) $info[$key]); if ($v !== '') { return $v; } } } } return 'STAFF'; } protected function normalizeIds(array $ids): array { return array_values(array_unique(array_filter(array_map(static function ($v) { if (is_string($v)) { $v = trim($v); } if ($v === '' || $v === null) { return null; } return (int) $v; }, $ids), static fn ($v) => $v !== null && $v > 0))); } protected function norm(?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); } protected function toPdf(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'; } protected function fitText(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; } protected function formatRole(?string $role): string { $role = (string) $role; $role = str_replace(['-', '_'], ' ', $role); $role = preg_replace('/\s+/u', ' ', trim($role)); if ($role === '') { return ''; } $special = [ 'of' => 'of', 'head' => 'Head', ]; $tokens = explode(' ', $role); $out = []; foreach ($tokens as $tok) { if ($tok === '') { continue; } $core = preg_replace('/^\P{L}+|\P{L}+$/u', '', $tok); $start = strpos($tok, $core); if ($start === false) { $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])) { $coreFmt = $special[$lw]; } elseif (preg_match('/^\p{L}{1,4}$/u', $core)) { $coreFmt = mb_strtoupper($core, 'UTF-8'); } else { $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); } protected function formatRolesCsv(?string $csv): string { if ($csv === null) { return ''; } $parts = array_filter(array_map('trim', explode(',', (string) $csv)), 'strlen'); if (empty($parts)) { return ''; } return implode(', ', array_map(fn ($r) => $this->formatRole($r), $parts)); } protected function formatClass(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; } protected function firstExisting(array $paths): ?string { foreach ($paths as $path) { if (! empty($path) && file_exists($path)) { return $path; } } return null; } protected function resolveClassName(int $classSectionId): ?string { $row = DB::table('classSection') ->select('class_section_name') ->where('class_section_id', $classSectionId) ->first(); return $row->class_section_name ?? null; } protected function getAvailableSchoolYears(): array { $schoolYears = []; try { if (DB::getSchemaBuilder()->hasTable('teacher_class')) { $schoolYears = DB::table('teacher_class') ->selectRaw('DISTINCT school_year') ->whereNotNull('school_year') ->orderByDesc('school_year') ->pluck('school_year') ->filter() ->values() ->all(); } } catch (\Throwable) { } if (empty($schoolYears)) { try { if ( DB::getSchemaBuilder()->hasTable('user_roles') && DB::getSchemaBuilder()->hasColumn('user_roles', 'school_year') ) { $schoolYears = DB::table('user_roles') ->selectRaw('DISTINCT school_year') ->whereNotNull('school_year') ->orderByDesc('school_year') ->pluck('school_year') ->filter() ->values() ->all(); } } catch (\Throwable) { } } return $schoolYears; } }