1, 'max' => 12, 'winners' => 2], ['min' => 13, 'max' => 16, 'winners' => 3], ['min' => 17, 'max' => 22, 'winners' => 4], ['min' => 23, 'max' => 27, 'winners' => 5], ['min' => 28, 'max' => null, 'winners' => 6], ]; public function __construct() { $this->db = \Config\Database::connect(); $this->classSectionModel = new ClassSectionModel(); $this->classWinnerModel = new CompetitionClassWinnerModel(); $this->configModel = new ConfigurationModel(); if ($this->db->tableExists('class_student')) { $this->classStudentTable = 'class_student'; } elseif ($this->db->tableExists('student_class')) { $this->classStudentTable = 'student_class'; } else { $this->classStudentTable = ''; } $this->hasClassWinnerTable = $this->db->tableExists('competition_class_winners'); $this->hasQuestionCount = $this->hasClassWinnerTable && $this->db->fieldExists('question_count', 'competition_class_winners'); } public function index() { $competitionModel = new CompetitionModel(); $competitions = $competitionModel ->orderBy('id', 'DESC') ->findAll(); return view('admin/competition_winners/index', [ 'competitions' => $competitions, 'sectionMap' => $this->getClassSectionMap(), ]); } public function create() { $schoolYear = $this->configModel->getConfig('school_year'); $classCounts = $this->getClassStudentCounts($schoolYear); $classSections = $this->classSectionModel ->orderBy('class_section_name', 'ASC') ->findAll(); $classSections = $this->filterSectionsWithStudents($classSections, $classCounts); $classRows = $this->buildClassRows($classSections, $classCounts, [], [], []); return view('admin/competition_winners/form', [ 'mode' => 'create', 'competition' => null, 'errors' => session('errors'), 'classSections' => $classSections, 'classRows' => $classRows, 'defaultSemester' => $this->configModel->getConfig('semester'), 'defaultSchoolYear'=> $schoolYear, ]); } public function settings($id) { $competitionModel = new CompetitionModel(); $competition = $competitionModel->find($id); if (!$competition) { return redirect()->to('/admin/competition-winners')->with('error', 'Competition not found.'); } $schoolYear = $competition['school_year'] ?? $this->configModel->getConfig('school_year'); $classCounts = $this->getClassStudentCounts($schoolYear); $classSections = $this->classSectionModel ->orderBy('class_section_name', 'ASC') ->findAll(); $classSections = $this->filterSectionsWithStudents($classSections, $classCounts); $settings = $this->getClassSettings((int) $id); $overrides = $settings['overrides']; $questionCounts = $settings['questions']; $prizeMap = $settings['prizes']; $limitClassId = (int) ($competition['class_section_id'] ?? 0); $classRows = $this->buildClassRows($classSections, $classCounts, $overrides, $questionCounts, $prizeMap, $limitClassId); return view('admin/competition_winners/form', [ 'mode' => 'edit', 'competition' => $competition, 'errors' => session('errors'), 'classSections' => $classSections, 'classRows' => $classRows, 'defaultSemester' => $this->configModel->getConfig('semester'), 'defaultSchoolYear'=> $schoolYear, ]); } public function store() { $rules = [ 'title' => 'required|min_length[3]', 'class_section_id' => 'permit_empty|integer', ]; if (!$this->validate($rules)) { return redirect()->back()->withInput()->with('errors', $this->validator->getErrors()); } $competitionModel = new CompetitionModel(); $classSectionId = $this->request->getPost('class_section_id'); $classSectionId = $classSectionId !== '' ? (int) $classSectionId : null; $competitionId = $competitionModel->insert([ 'title' => $this->request->getPost('title'), 'semester' => $this->request->getPost('semester') ?: null, 'school_year' => $this->request->getPost('school_year') ?: null, 'class_section_id' => $classSectionId ?: null, 'start_date' => $this->request->getPost('start_date') ?: null, 'end_date' => $this->request->getPost('end_date') ?: null, 'created_by' => session('user_id') ?: null, ], true); $overrides = (array) $this->request->getPost('winner_overrides'); $questionCounts = (array) $this->request->getPost('question_counts'); $prizes = (array) $this->request->getPost('prizes'); $this->saveClassSettings((int) $competitionId, $overrides, $questionCounts, $prizes, $classSectionId ? (int) $classSectionId : null); return redirect()->to('/admin/competition-winners')->with('success', 'Competition created.'); } public function update($id) { $competitionModel = new CompetitionModel(); $competition = $competitionModel->find($id); if (!$competition) { return redirect()->to('/admin/competition-winners')->with('error', 'Competition not found.'); } if (!empty($competition['is_locked'])) { return redirect()->back()->with('error', 'Competition is locked. Unlock it to make changes.'); } $rules = [ 'title' => 'required|min_length[3]', 'class_section_id' => 'permit_empty|integer', ]; if (!$this->validate($rules)) { return redirect()->back()->withInput()->with('errors', $this->validator->getErrors()); } $classSectionId = $this->request->getPost('class_section_id'); $classSectionId = $classSectionId !== '' ? (int) $classSectionId : null; $competitionModel->update($id, [ 'title' => $this->request->getPost('title'), 'semester' => $this->request->getPost('semester') ?: null, 'school_year' => $this->request->getPost('school_year') ?: null, 'class_section_id' => $classSectionId ?: null, 'start_date' => $this->request->getPost('start_date') ?: null, 'end_date' => $this->request->getPost('end_date') ?: null, ]); $overrides = (array) $this->request->getPost('winner_overrides'); $questionCounts = (array) $this->request->getPost('question_counts'); $prizes = (array) $this->request->getPost('prizes'); $this->saveClassSettings((int) $id, $overrides, $questionCounts, $prizes, $classSectionId ? (int) $classSectionId : null); return redirect()->to('/admin/competition-winners')->with('success', 'Competition updated.'); } public function edit($id) { $competitionModel = new CompetitionModel(); $scoreModel = new CompetitionScoreModel(); $studentModel = new StudentModel(); $competition = $competitionModel->find($id); if (!$competition) { return redirect()->to('/admin/competition-winners')->with('error', 'Competition not found.'); } $lockedClassSectionId = (int) ($competition['class_section_id'] ?? 0); $selectedClassSectionId = (int) ($this->request->getGet('class_section_id') ?? 0); $activeClassSectionId = $lockedClassSectionId > 0 ? $lockedClassSectionId : $selectedClassSectionId; $students = []; $scoreMap = []; if ($activeClassSectionId > 0) { $students = $this->getStudentsForCompetition($studentModel, $competition, $activeClassSectionId); $existingScores = $scoreModel ->where('competition_id', $id) ->where('class_section_id', $activeClassSectionId) ->findAll(); foreach ($existingScores as $row) { $scoreMap[$row['student_id']] = $row['score']; } } $schoolYear = $competition['school_year'] ?? null; $classCounts = $this->getClassStudentCounts($schoolYear); $classSections = $this->classSectionModel ->orderBy('class_section_name', 'ASC') ->findAll(); $classSections = $this->filterSectionsWithStudents($classSections, $classCounts); $settings = $this->getClassSettings((int) $id); $overrides = $settings['overrides']; $questionCounts = $settings['questions']; $prizeMap = $settings['prizes']; $classStudentCount = $classCounts[$activeClassSectionId] ?? 0; $classOverride = $overrides[$activeClassSectionId] ?? null; $classAuto = $this->winnersForCount($classStudentCount); $classFinal = $classOverride !== null ? $classOverride : $classAuto; $classQuestionCount = $questionCounts[$activeClassSectionId] ?? null; return view('admin/competition_winners/scores', [ 'competition' => $competition, 'students' => $students, 'scoreMap' => $scoreMap, 'classSections' => $classSections, 'activeClassSectionId' => $activeClassSectionId, 'classSectionName' => $this->getClassSectionName($activeClassSectionId), 'classSelectionLocked' => $lockedClassSectionId > 0, 'classStudentCount' => $classStudentCount, 'classAutoWinners' => $classAuto, 'classOverrideWinners' => $classOverride, 'classFinalWinners' => $classFinal, 'classQuestionCount' => $classQuestionCount, 'classPrizeMap' => $prizeMap, ]); } public function saveScores($id) { $competitionModel = new CompetitionModel(); $scoreModel = new CompetitionScoreModel(); $competition = $competitionModel->find($id); if (!$competition) { return redirect()->to('/admin/competition-winners')->with('error', 'Competition not found.'); } if (!empty($competition['is_locked'])) { return redirect()->back()->with('error', 'Competition is locked. Unlock it to update scores.'); } $classSectionId = (int) ($competition['class_section_id'] ?? 0); if ($classSectionId <= 0) { $classSectionId = (int) $this->request->getPost('class_section_id'); } if ($classSectionId <= 0) { return redirect()->back()->with('error', 'Select a class section before saving scores.'); } $scores = (array) $this->request->getPost('scores'); foreach ($scores as $studentId => $scoreValue) { $scoreValue = trim((string) $scoreValue); if ($scoreValue === '') { continue; } $scoreValue = (float) $scoreValue; $existing = $scoreModel ->where('competition_id', $id) ->where('student_id', (int) $studentId) ->where('class_section_id', $classSectionId) ->first(); if ($existing) { $scoreModel->update($existing['id'], [ 'score' => $scoreValue, 'class_section_id' => $classSectionId, ]); } else { $scoreModel->insert([ 'competition_id' => (int) $id, 'student_id' => (int) $studentId, 'class_section_id' => $classSectionId, 'score' => $scoreValue, ]); } } return redirect()->to("/admin/competition-winners/{$id}/preview")->with('success', 'Scores saved.'); } public function preview($id) { $competitionModel = new CompetitionModel(); $scoreModel = new CompetitionScoreModel(); $competition = $competitionModel->find($id); if (!$competition) { return redirect()->to('/admin/competition-winners')->with('error', 'Competition not found.'); } $rows = $scoreModel ->select('competition_scores.*, s.firstname, s.lastname') ->join('students s', 's.id = competition_scores.student_id', 'left') ->where('competition_id', $id) ->findAll(); $rankedByClass = $this->groupScoresByClass($rows); $scoreCounts = $this->getScoreCountsByClass($rankedByClass); $classCounts = $this->getClassStudentCounts($competition['school_year'] ?? null); $settings = $this->getClassSettings((int) $id); $overrides = $settings['overrides']; $questionCounts = $settings['questions']; $prizeMap = $settings['prizes']; $winnersCountByClass = $this->getWinnersCountByClass($competition, $classCounts, $scoreCounts, $overrides); $topByClass = $this->getTopWinnersByClass( $rankedByClass, $winnersCountByClass, $prizeMap, $questionCounts, $classCounts, $scoreCounts ); return view('admin/competition_winners/preview', [ 'competition' => $competition, 'classMap' => $this->getClassSectionMap(), 'classCounts' => $classCounts, 'overrideMap' => $overrides, 'questionCounts' => $questionCounts, 'prizeMap' => $prizeMap, 'winnersCountByClass'=> $winnersCountByClass, 'rankedByClass' => $rankedByClass, 'topByClass' => $topByClass, ]); } public function exportCompetitionToQuiz(int $competitionId, int $classSectionId = 0) { if ($competitionId <= 0) { return redirect()->back()->with('error', 'Competition not found.'); } $competitionModel = new CompetitionModel(); $competition = $competitionModel->find($competitionId); if (!$competition) { return redirect()->back()->with('error', 'Competition not found.'); } $lockedClassSectionId = (int) ($competition['class_section_id'] ?? 0); if ($lockedClassSectionId > 0) { $classSectionId = $lockedClassSectionId; } if ($classSectionId > 0 && $lockedClassSectionId > 0 && $lockedClassSectionId !== $classSectionId) { return redirect()->back()->with('error', 'This competition is locked to a different class section.'); } $db = $this->db ?: db_connect(); $schoolId = session('school_id'); $updatedBy = session('user_id'); $semester = session('semester'); $schoolYear = session('school_year'); if ($semester === null || $semester === '') { $semester = $this->configModel->getConfig('semester'); } if ($schoolYear === null || $schoolYear === '') { $schoolYear = $this->configModel->getConfig('school_year'); } $classSectionIds = []; if ($classSectionId > 0) { $classSectionIds = [$classSectionId]; } else { $rows = $db->table('competition_scores') ->select('class_section_id') ->distinct() ->where('competition_id', $competitionId) ->where('class_section_id >', 0) ->get()->getResultArray(); $classSectionIds = array_values(array_filter(array_map(static function ($row) { return (int) ($row['class_section_id'] ?? 0); }, $rows))); } if (empty($classSectionIds)) { return redirect()->back()->with('error', 'No class sections with scores found for this competition.'); } $db->transStart(); $quizIndexes = []; foreach ($classSectionIds as $cid) { $cid = (int) $cid; if ($cid <= 0) { continue; } // 1) next quiz_index $row = $db->query( "SELECT COALESCE(MAX(quiz_index), 0) + 1 AS next_index FROM quiz WHERE class_section_id = ? AND semester = ? AND school_year = ?", [$cid, $semester, $schoolYear] )->getRowArray(); $quizIndex = (int) ($row['next_index'] ?? 1); // 2) insert normalized competition scores $db->query( "INSERT INTO quiz (student_id, school_id, class_section_id, updated_by, quiz_index, score, comment, semester, school_year, created_at, updated_at) SELECT cs.student_id, ?, cs.class_section_id, ?, ?, ROUND(LEAST(100, GREATEST(0, (cs.score / NULLIF(ccw.question_count, 0)) * 100)), 2), CONCAT('Competition ', cs.competition_id, ' normalized from ', cs.score, '/', ccw.question_count), ?, ?, NOW(), NOW() FROM competition_scores cs JOIN competition_class_winners ccw ON ccw.competition_id = cs.competition_id AND ccw.class_section_id = cs.class_section_id WHERE cs.competition_id = ? AND cs.class_section_id = ?", [$schoolId, $updatedBy, $quizIndex, $semester, $schoolYear, $competitionId, $cid] ); $quizIndexes[$cid] = $quizIndex; } $db->transComplete(); if (!$db->transStatus()) { return redirect()->back()->with('error', 'Failed exporting competition scores.'); } if (empty($quizIndexes)) { return redirect()->back()->with('error', 'No scores were exported.'); } if (count($quizIndexes) === 1) { $quizIndex = array_values($quizIndexes)[0]; return redirect()->back()->with('success', "Exported as Quiz {$quizIndex}."); } $classMap = $this->getClassSectionMap(); $labels = []; foreach ($quizIndexes as $cid => $quizIndex) { $label = $classMap[$cid] ?? $cid; $labels[] = "{$label}: Quiz {$quizIndex}"; } $detail = implode(', ', $labels); return redirect()->back()->with('success', "Exported competition scores. {$detail}."); } public function winners($id) { $competitionModel = new CompetitionModel(); $winnerModel = new CompetitionWinnerModel(); $classWinnerModel = new CompetitionClassWinnerModel(); $competition = $competitionModel->find($id); if (!$competition) { return redirect()->to('/admin/competition-winners')->with('error', 'Competition not found.'); } $rows = $winnerModel ->select('competition_winners.*, s.firstname, s.lastname, s.school_id, s.photo_consent, cs.class_section_name') ->join('students s', 's.id = competition_winners.student_id', 'left') ->join('classSection cs', 'cs.class_section_id = competition_winners.class_section_id', 'left') ->where('competition_id', $id) ->orderBy('competition_winners.class_section_id', 'ASC') ->orderBy('rank', 'ASC') ->findAll(); $questionCounts = $this->getQuestionCountsForCompetition((int) $id); return view('admin/competition_winners/winners', [ 'competition' => $competition, 'rows' => $rows, 'classMap' => $this->getClassSectionMap(), 'questionCounts' => $questionCounts, ]); } public function recognitionPdf($id) { $competitionModel = new CompetitionModel(); $winnerModel = new CompetitionWinnerModel(); $classWinnerModel = new CompetitionClassWinnerModel(); $competition = $competitionModel->find($id); if (!$competition) { return redirect()->to('/admin/competition-winners')->with('error', 'Competition not found.'); } $rows = $winnerModel ->select('competition_winners.*, s.firstname, s.lastname, s.school_id, cs.class_section_name') ->join('students s', 's.id = competition_winners.student_id', 'left') ->join('classSection cs', 'cs.class_section_id = competition_winners.class_section_id', 'left') ->where('competition_id', $id) ->orderBy('competition_winners.class_section_id', 'ASC') ->orderBy('rank', 'ASC') ->findAll(); if (empty($rows)) { return 'No published winners yet.'; } $questionCounts = $this->getQuestionCountsForCompetition((int) $id); $classMap = $this->getClassSectionMap(); if (ENVIRONMENT !== 'production') { unset(service('toolbar')->collectors['CodeIgniter\Debug\Toolbar\Collectors\Timers']); } $normalize = static function (string $text): string { $text = str_replace(["\xC2\xA0", "\xA0"], ' ', $text); $text = preg_replace('/\s+/u', ' ', $text) ?? $text; $text = preg_replace('/[\x00-\x08\x0B-\x0C\x0E-\x1F\x7F]/u', '', $text) ?? $text; return trim($text); }; $toPdf = static function (string $text): string { if (function_exists('iconv')) { $out = @iconv('UTF-8', 'ISO-8859-1//TRANSLIT', $text); if ($out !== false && $out !== '') { return $out; } } if (function_exists('mb_convert_encoding')) { $out = @mb_convert_encoding($text, 'ISO-8859-1', 'UTF-8'); if ($out !== '') { return $out; } } $out = @utf8_decode($text); return $out !== '' ? $out : $text; }; $formatScore = static function ($scoreValue, ?int $questionCount): string { if ($scoreValue === null || $scoreValue === '' || !is_numeric($scoreValue)) { return '-'; } $scoreDisplay = number_format((float) $scoreValue, 0, '.', ''); if ($questionCount !== null) { return $scoreDisplay . '/' . $questionCount; } return $scoreDisplay; }; $formatOrdinal = static function ($value): string { if ($value === null || $value === '' || !is_numeric($value)) { return '-'; } $num = (int) $value; if ($num <= 0) { return '-'; } $suffix = 'th'; if ($num % 100 < 11 || $num % 100 > 13) { switch ($num % 10) { case 1: $suffix = 'st'; break; case 2: $suffix = 'nd'; break; case 3: $suffix = 'rd'; break; } } return $num . $suffix; }; $fitText = static function (\FPDF $pdf, string $text, float $width) use ($normalize, $toPdf): string { $suffix = '...'; $text = $normalize($text); $encoded = $toPdf($text); if ($pdf->GetStringWidth($encoded) <= $width) { return $encoded; } $maxWidth = $width - $pdf->GetStringWidth($suffix); if ($maxWidth <= 0) { return $suffix; } $trimmed = $text; while ($trimmed !== '') { if (function_exists('mb_substr')) { $trimmed = mb_substr($trimmed, 0, -1); } else { $trimmed = substr($trimmed, 0, -1); } $encoded = $toPdf($trimmed); if ($pdf->GetStringWidth($encoded) <= $maxWidth) { return $encoded . $suffix; } } return $suffix; }; $pdf = new \FPDF('P', 'mm', 'Letter'); $leftMargin = 15; $rightMargin = 15; $topMargin = 15; $pdf->SetMargins($leftMargin, $topMargin, $rightMargin); $pdf->SetAutoPageBreak(true, 15); $logoPath = FCPATH . 'assets/images/alrahma_logo.png'; if (!is_file($logoPath)) { $logoPath = FCPATH . 'images/alrahma_logo.png'; } if (!is_file($logoPath)) { $logoPath = null; } $signaturePath = FCPATH . 'assets/images/signature.png'; if (!is_file($signaturePath)) { $signaturePath = null; } $drawField = static function ( \FPDF $pdf, float $x, float $y, float $width, string $label, string $value, callable $fitText, callable $toPdf ): void { $pdf->SetXY($x, $y); $pdf->SetFont('Arial', 'B', 11); $pdf->Write(5, $toPdf($label)); $labelWidth = $pdf->GetStringWidth($toPdf($label)); $pdf->SetFont('Arial', '', 11); $available = max(1, $width - $labelWidth - 2); $valueText = $fitText($pdf, $value, $available); $pdf->SetXY($x + $labelWidth + 2, $y); $pdf->Write(5, $valueText); }; $competitionTitle = $normalize((string) ($competition['title'] ?? '')); $competitionLabel = $competitionTitle !== '' ? $competitionTitle : 'competition'; if (stripos($competitionLabel, 'competition') === false) { $competitionLabel .= ' competition'; } $eventDate = (new \DateTime('now'))->format('m/d/Y'); $pageWidth = $pdf->GetPageWidth(); $pageHeight = $pdf->GetPageHeight(); $bottomMargin = 15; $blockHeight = ($pageHeight - $topMargin - $bottomMargin) / 2; $drawStudentBlock = static function ( \FPDF $pdf, array $row, float $blockTop, float $pageWidth, float $leftMargin, float $rightMargin, ?string $logoPath, ?string $signaturePath, array $classMap, array $questionCounts, callable $formatScore, callable $formatOrdinal, callable $fitText, callable $toPdf, callable $normalize, string $competitionLabel, string $eventDate, callable $drawField ): void { $contentWidth = $pageWidth - $leftMargin - $rightMargin; $logoX = $leftMargin; $logoY = $blockTop + 5; $logoW = 22; if ($logoPath) { $pdf->Image($logoPath, $logoX, $logoY, $logoW); } $infoX = $logoX + $logoW + 6; $infoY = $blockTop + 8; $availableW = $pageWidth - $infoX - $rightMargin; $leftW = $availableW * 0.48; $midW = $availableW * 0.32; $rightW = $availableW - $leftW - $midW; $classId = (int) ($row['class_section_id'] ?? 0); $classLabel = (string) ($row['class_section_name'] ?? ($classMap[$classId] ?? ('Class ' . $classId))); if (strtolower(trim($classLabel)) === 'youth') { $classLabel = 'Youth'; } $name = trim(($row['firstname'] ?? '') . ' ' . ($row['lastname'] ?? '')); $studentLabel = $name !== '' ? $name : ('Student #' . ($row['student_id'] ?? '')); $scoreLabel = $formatScore($row['score'] ?? null, $questionCounts[$classId] ?? null); $placementLabel = $formatOrdinal($row['rank'] ?? null); $prizeValue = null; if (isset($row['prize_amount']) && $row['prize_amount'] !== '' && is_numeric($row['prize_amount'])) { $prizeValue = (float) $row['prize_amount']; } $prizeLabel = $prizeValue !== null ? ('$' . number_format($prizeValue, 2, '.', '')) : '-'; $drawField($pdf, $infoX, $infoY, $leftW, 'Student Name: ', $studentLabel, $fitText, $toPdf); $drawField($pdf, $infoX + $leftW, $infoY, $midW + $rightW, 'Grade: ', $classLabel, $fitText, $toPdf); $line2Y = $infoY + 7; $drawField($pdf, $infoX, $line2Y, $leftW, 'Quiz Score: ', $scoreLabel, $fitText, $toPdf); $drawField($pdf, $infoX + $leftW, $line2Y, $midW, 'Final Placement: ', $placementLabel, $fitText, $toPdf); $drawField($pdf, $infoX + $leftW + $midW, $line2Y, $rightW, 'Prize: ', $prizeLabel, $fitText, $toPdf); $bodyY = $line2Y + 16; $pdf->SetXY($leftMargin, $bodyY); $pdf->SetFont('Arial', '', 11); $firstParagraph = $eventDate !== '' ? sprintf( 'Heartfelt congratulations on your exceptional performance in the %s held by Al Rahma Sunday School on %s!', $competitionLabel, $eventDate ) : sprintf( 'Heartfelt congratulations on your exceptional performance in the %s held by Al Rahma Sunday School!', $competitionLabel ); $paragraphs = [ $firstParagraph, 'Your commitment to excellence in the competition reflects not only your knowledge but also your passion for the subject. May this achievement be a stepping stone to even greater successes in the future.', 'Wishing you continued success and fulfillment in your academic journey.', ]; foreach ($paragraphs as $paragraph) { $pdf->MultiCell($contentWidth, 6, $toPdf($normalize($paragraph)), 0, 'L'); $pdf->Ln(2); } $pdf->Ln(2); $pdf->SetFont('Arial', '', 11); $pdf->Cell(0, 6, $toPdf('Warmest congratulations,'), 0, 1, 'L'); $pdf->Cell(0, 6, $toPdf('- Al Rahma Principal'), 0, 1, 'L'); if ($signaturePath) { $pdf->Ln(2); $sigX = $leftMargin + 2; $sigY = $pdf->GetY(); $pdf->Image($signaturePath, $sigX, $sigY, 35); } }; foreach (array_values($rows) as $index => $row) { if ($index % 2 === 0) { $pdf->AddPage(); } $blockTop = $topMargin + ($blockHeight * ($index % 2)); $drawStudentBlock( $pdf, $row, $blockTop, $pageWidth, $leftMargin, $rightMargin, $logoPath, $signaturePath, $classMap, $questionCounts, $formatScore, $formatOrdinal, $fitText, $toPdf, $normalize, $competitionLabel, $eventDate, $drawField ); } if (ob_get_length()) { ob_end_clean(); } $pdfContent = $pdf->Output('S'); $filename = 'Competition_Winners_Recognition_' . (int) $id . '.pdf'; return $this->response ->setHeader('Content-Type', 'application/pdf') ->setHeader('Content-Disposition', 'inline; filename="' . $filename . '"') ->setHeader('Cache-Control', 'private, max-age=0, must-revalidate') ->setHeader('Pragma', 'public') ->setHeader('Content-Length', (string) strlen($pdfContent)) ->setBody($pdfContent); } public function publish($id) { $competitionModel = new CompetitionModel(); $winnerModel = new CompetitionWinnerModel(); $scoreModel = new CompetitionScoreModel(); $competition = $competitionModel->find($id); if (!$competition) { return redirect()->to('/admin/competition-winners')->with('error', 'Competition not found.'); } if (!empty($competition['is_locked'])) { return redirect()->back()->with('error', 'Competition is locked. Unlock it to publish winners.'); } $rows = $scoreModel ->select('competition_scores.*') ->where('competition_id', $id) ->findAll(); $rankedByClass = $this->groupScoresByClass($rows); $scoreCounts = $this->getScoreCountsByClass($rankedByClass); $classCounts = $this->getClassStudentCounts($competition['school_year'] ?? null); $settings = $this->getClassSettings((int) $id); $overrides = $settings['overrides']; $questionCounts = $settings['questions']; $prizeMap = $settings['prizes']; $winnersCountByClass = $this->getWinnersCountByClass($competition, $classCounts, $scoreCounts, $overrides); $winnerModel->where('competition_id', $id)->delete(); foreach ($rankedByClass as $classSectionId => $rowsForClass) { $limit = (int) ($winnersCountByClass[$classSectionId] ?? 0); if ($limit <= 0) { continue; } $topWithPrizes = $this->assignPrizesByScore( $rowsForClass, $prizeMap[$classSectionId] ?? [], $limit, $questionCounts[$classSectionId] ?? null ); foreach ($topWithPrizes as $row) { $winnerModel->insert([ 'competition_id' => (int) $id, 'student_id' => (int) $row['student_id'], 'class_section_id' => (int) $classSectionId, 'rank' => (int) $row['rank'], 'score' => (float) $row['score'], 'prize_amount' => (float) $row['prize_amount'], 'created_at' => date('Y-m-d H:i:s'), ]); } } $competitionModel->update($id, [ 'is_published' => 1, 'published_at' => date('Y-m-d H:i:s'), ]); return redirect()->to('/admin/competition-winners')->with('success', 'Winners published.'); } public function lock($id) { $competitionModel = new CompetitionModel(); $competition = $competitionModel->find($id); if (!$competition) { return redirect()->to('/admin/competition-winners')->with('error', 'Competition not found.'); } if (!empty($competition['is_locked'])) { return redirect()->back()->with('success', 'Competition is already locked.'); } $competitionModel->update($id, [ 'is_locked' => 1, 'locked_at' => date('Y-m-d H:i:s'), 'locked_by' => session('user_id') ?: null, ]); return redirect()->back()->with('success', 'Competition locked.'); } public function unlock($id) { $competitionModel = new CompetitionModel(); $competition = $competitionModel->find($id); if (!$competition) { return redirect()->to('/admin/competition-winners')->with('error', 'Competition not found.'); } if (empty($competition['is_locked'])) { return redirect()->back()->with('success', 'Competition is already unlocked.'); } $competitionModel->update($id, [ 'is_locked' => 0, 'locked_at' => null, 'locked_by' => null, ]); return redirect()->back()->with('success', 'Competition unlocked.'); } private function buildClassRows(array $classSections, array $classCounts, array $overrides, array $questionCounts, array $prizeMap, int $limitClassId = 0): array { $rows = []; foreach ($classSections as $section) { $classId = (int) ($section['class_section_id'] ?? 0); if ($classId <= 0) { continue; } if ($limitClassId > 0 && $classId !== $limitClassId) { continue; } $count = $classCounts[$classId] ?? 0; if ($count <= 0) { continue; } $auto = $this->winnersForCount($count); $override = isset($overrides[$classId]) ? (int) $overrides[$classId] : null; $questionCount = isset($questionCounts[$classId]) ? (int) $questionCounts[$classId] : null; $prizes = $prizeMap[$classId] ?? []; $rows[] = [ 'class_section_id' => $classId, 'class_section_name' => $section['class_section_name'] ?? (string) $classId, 'student_count' => $count, 'auto_winners' => $auto, 'override_winners' => $override, 'final_winners' => $override !== null ? $override : $auto, 'question_count' => $questionCount, 'prize_1' => $prizes[1] ?? null, 'prize_2' => $prizes[2] ?? null, 'prize_3' => $prizes[3] ?? null, 'prize_4' => $prizes[4] ?? null, 'prize_5' => $prizes[5] ?? null, 'prize_6' => $prizes[6] ?? null, ]; } return $rows; } private function filterSectionsWithStudents(array $classSections, array $classCounts): array { return array_values(array_filter($classSections, static function ($section) use ($classCounts) { $classId = (int) ($section['class_section_id'] ?? 0); if ($classId <= 0) { return false; } return ($classCounts[$classId] ?? 0) > 0; })); } private function saveClassSettings(int $competitionId, array $overrides, array $questionCounts, array $prizes, ?int $limitClassId = null): void { $classIds = array_unique(array_merge(array_keys($overrides), array_keys($questionCounts), array_keys($prizes))); foreach ($classIds as $classId) { $classId = (int) $classId; if ($classId <= 0) { continue; } if ($limitClassId !== null && $classId !== $limitClassId) { continue; } $rawOverride = trim((string) ($overrides[$classId] ?? '')); $overrideValue = $rawOverride === '' ? null : (int) $rawOverride; $rawQuestions = trim((string) ($questionCounts[$classId] ?? '')); $questionValue = $rawQuestions === '' ? null : (int) $rawQuestions; $existing = $this->classWinnerModel ->where('competition_id', $competitionId) ->where('class_section_id', $classId) ->first(); $prizeValues = $this->normalizePrizeInputs($prizes[$classId] ?? []); $hasPrize = array_filter($prizeValues, static fn($v) => $v !== null); if ($overrideValue === null && $questionValue === null && empty($hasPrize)) { if ($existing) { $this->classWinnerModel->delete($existing['id']); } continue; } $payload = [ 'competition_id' => $competitionId, 'class_section_id' => $classId, 'winners_override' => $overrideValue, 'prize_1' => $prizeValues[1], 'prize_2' => $prizeValues[2], 'prize_3' => $prizeValues[3], 'prize_4' => $prizeValues[4], 'prize_5' => $prizeValues[5], 'prize_6' => $prizeValues[6], ]; if ($this->hasQuestionCount) { $payload['question_count'] = $questionValue; } if ($existing) { $this->classWinnerModel->update($existing['id'], $payload); } else { $this->classWinnerModel->insert($payload); } } } private function getClassSettings(int $competitionId): array { $rows = $this->classWinnerModel ->where('competition_id', $competitionId) ->findAll(); $overrides = []; $questions = []; $prizes = []; foreach ($rows as $row) { $classId = (int) ($row['class_section_id'] ?? 0); if ($classId <= 0) { continue; } if ($row['winners_override'] !== null) { $overrides[$classId] = (int) $row['winners_override']; } if ($this->hasQuestionCount && array_key_exists('question_count', $row) && $row['question_count'] !== null) { $questions[$classId] = (int) $row['question_count']; } $prizes[$classId] = [ 1 => $this->castPrize($row['prize_1'] ?? null), 2 => $this->castPrize($row['prize_2'] ?? null), 3 => $this->castPrize($row['prize_3'] ?? null), 4 => $this->castPrize($row['prize_4'] ?? null), 5 => $this->castPrize($row['prize_5'] ?? null), 6 => $this->castPrize($row['prize_6'] ?? null), ]; } return [ 'overrides' => $overrides, 'questions' => $questions, 'prizes' => $prizes, ]; } private function getQuestionCountsForCompetition(int $competitionId): array { if (!$this->hasQuestionCount || !$this->hasClassWinnerTable) { return []; } $qRows = $this->classWinnerModel ->select('class_section_id, question_count') ->where('competition_id', $competitionId) ->findAll(); $questionCounts = []; foreach ($qRows as $row) { $classId = (int) ($row['class_section_id'] ?? 0); if ($classId > 0 && $row['question_count'] !== null) { $questionCounts[$classId] = (int) $row['question_count']; } } return $questionCounts; } private function getClassSectionMap(): array { $sections = $this->classSectionModel ->select('class_section_id, class_section_name') ->orderBy('class_section_name', 'ASC') ->findAll(); $map = []; foreach ($sections as $section) { $id = (int) ($section['class_section_id'] ?? 0); if ($id > 0) { $map[$id] = $section['class_section_name'] ?? (string) $id; } } return $map; } private function getClassSectionName(int $classSectionId): ?string { if ($classSectionId <= 0) { return null; } $row = $this->classSectionModel ->select('class_section_name') ->where('class_section_id', $classSectionId) ->first(); return $row['class_section_name'] ?? null; } private function getStudentsForCompetition(StudentModel $studentModel, array $competition, int $classSectionId): array { if ($classSectionId <= 0 || $this->classStudentTable === '') { return []; } $builder = $this->db->table('students s') ->select('s.id, s.school_id, s.firstname, s.lastname') ->join($this->classStudentTable . ' cs', 'cs.student_id = s.id', 'inner') ->where('cs.class_section_id', $classSectionId); $hasSchoolYear = $this->db->fieldExists('school_year', $this->classStudentTable); if ($hasSchoolYear && !empty($competition['school_year'])) { $builder->where('cs.school_year', $competition['school_year']); } $builder->distinct(); $builder->orderBy('s.lastname', 'ASC') ->orderBy('s.firstname', 'ASC'); return $builder->get()->getResultArray(); } private function getClassStudentCounts(?string $schoolYear): array { if ($this->classStudentTable === '') { return []; } $builder = $this->db->table($this->classStudentTable) ->select('class_section_id, COUNT(*) AS total') ->where('class_section_id IS NOT NULL', null, false) ->groupBy('class_section_id'); if ($schoolYear && $this->db->fieldExists('school_year', $this->classStudentTable)) { $builder->where('school_year', $schoolYear); } $rows = $builder->get()->getResultArray(); $map = []; foreach ($rows as $row) { $classId = (int) ($row['class_section_id'] ?? 0); if ($classId > 0) { $map[$classId] = (int) ($row['total'] ?? 0); } } return $map; } private function winnersForCount(int $count): int { if ($count <= 0) { return 0; } foreach ($this->winnerTiers as $tier) { $min = (int) $tier['min']; $max = $tier['max']; if ($count < $min) { continue; } if ($max === null || $count <= (int) $max) { return (int) $tier['winners']; } } return 0; } private function getWinnersCountByClass(array $competition, array $classCounts, array $scoreCounts, array $overrides): array { $classIds = array_unique(array_merge( array_keys($classCounts), array_keys($scoreCounts), array_keys($overrides) )); $limitClassId = (int) ($competition['class_section_id'] ?? 0); $map = []; foreach ($classIds as $classId) { $classId = (int) $classId; if ($classId <= 0) { continue; } if ($limitClassId > 0 && $classId !== $limitClassId) { continue; } $override = $overrides[$classId] ?? null; if ($override !== null) { $map[$classId] = (int) $override; continue; } $baseCount = $classCounts[$classId] ?? ($scoreCounts[$classId] ?? 0); if ($baseCount <= 0) { $map[$classId] = 0; continue; } $maxWinners = $this->winnersForCount((int) $baseCount); $minWinners = 3; $map[$classId] = $maxWinners >= $minWinners ? $maxWinners : $minWinners; } return $map; } private function groupScoresByClass(array $rows): array { $grouped = []; foreach ($rows as $row) { $classSectionId = (int) ($row['class_section_id'] ?? 0); if ($classSectionId <= 0) { continue; } $name = trim(($row['firstname'] ?? '') . ' ' . ($row['lastname'] ?? '')); $score = (float) ($row['score'] ?? 0); $grouped[$classSectionId][] = [ 'student_id' => $row['student_id'], 'name' => $name !== '' ? $name : ('Student #' . $row['student_id']), 'score' => $score, ]; } foreach ($grouped as &$rowsForClass) { usort($rowsForClass, static function ($a, $b) { return $b['score'] <=> $a['score']; }); } unset($rowsForClass); return $grouped; } private function getScoreCountsByClass(array $rankedByClass): array { $counts = []; foreach ($rankedByClass as $classId => $rows) { $counts[(int) $classId] = count($rows); } return $counts; } private function getTopWinnersByClass( array $rankedByClass, array $winnersCountByClass, array $prizeMap, array $questionCounts, array $classCounts, array $scoreCounts ): array { $out = []; foreach ($rankedByClass as $classSectionId => $rows) { $limit = (int) ($winnersCountByClass[$classSectionId] ?? 0); if ($limit <= 0) { continue; } $out[$classSectionId] = $this->assignPrizesByScore( $rows, $prizeMap[$classSectionId] ?? [], $limit, $questionCounts[$classSectionId] ?? null ); } return $out; } private function normalizePrizeInputs(array $prizes): array { $out = [1 => null, 2 => null, 3 => null, 4 => null, 5 => null, 6 => null]; foreach ($out as $rank => $value) { $raw = trim((string) ($prizes[$rank] ?? '')); if ($raw === '') { $out[$rank] = null; } else { $out[$rank] = (float) $raw; } } return $out; } private function castPrize($value): ?float { if ($value === null || $value === '') { return null; } return (float) $value; } private function getPrizeForRank(array $classPrizes, int $rank): float { $val = $classPrizes[$rank] ?? null; return $val !== null ? (float) $val : 0.0; } private function assignPrizesByScore( array $rows, array $classPrizes, int $limit, ?int $questionCount = null ): array { if ($limit <= 0) { return []; } usort($rows, static function ($a, $b) { return ($b['score'] ?? 0) <=> ($a['score'] ?? 0); }); $scoreGroups = []; $scoreOrder = []; foreach ($rows as $row) { $score = (float) ($row['score'] ?? 0); $scoreKey = number_format($score, 5, '.', ''); if (!array_key_exists($scoreKey, $scoreGroups)) { $scoreGroups[$scoreKey] = []; $scoreOrder[] = $scoreKey; } $scoreGroups[$scoreKey][] = $row; } $top = []; $rank = 1; foreach ($scoreOrder as $scoreKey) { if (count($top) >= $limit) { break; } foreach ($scoreGroups[$scoreKey] as $row) { $row['rank'] = $rank; $top[] = $row; } $rank++; } $sharedPrize = null; if ($questionCount !== null && $questionCount > 0 && !empty($top)) { $allMax = true; foreach ($top as $row) { $score = (float) ($row['score'] ?? 0); if ($score < $questionCount) { $allMax = false; break; } } if ($allMax) { $sharedPrize = $this->getPrizeForRank($classPrizes, 1); } } $out = []; foreach ($top as $row) { if ($sharedPrize !== null) { $prize = $sharedPrize; } else { $prize = $this->getPrizeForRank($classPrizes, (int) ($row['rank'] ?? 1)); } $row['prize_amount'] = $prize; $out[] = $row; } return $out; } }