1398 lines
52 KiB
PHP
Executable File
1398 lines
52 KiB
PHP
Executable File
<?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;
|
|
}
|
|
}
|