Files
alrahma_sunday_school/app/Controllers/Admin/CompetitionWinnersController.php
2026-02-10 22:11:06 -05:00

1398 lines
52 KiB
PHP

<?php
namespace App\Controllers\Admin;
use App\Controllers\BaseController;
use App\Models\ClassSectionModel;
use App\Models\CompetitionClassWinnerModel;
use App\Models\CompetitionModel;
use App\Models\CompetitionScoreModel;
use App\Models\CompetitionWinnerModel;
use App\Models\ConfigurationModel;
use App\Models\StudentModel;
require_once APPPATH . 'ThirdParty/fpdf/fpdf.php';
class CompetitionWinnersController extends BaseController
{
protected $db;
protected ClassSectionModel $classSectionModel;
protected CompetitionClassWinnerModel $classWinnerModel;
protected ConfigurationModel $configModel;
protected string $classStudentTable;
protected bool $hasClassWinnerTable = false;
protected bool $hasQuestionCount = false;
private array $winnerTiers = [
['min' => 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;
}
}