update controllers logic
This commit is contained in:
@@ -0,0 +1,742 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Admin\CompetitionWinners;
|
||||
|
||||
use App\Models\ClassSection;
|
||||
use App\Models\Competition;
|
||||
use App\Models\CompetitionClassWinner;
|
||||
use App\Models\CompetitionScore;
|
||||
use App\Models\CompetitionWinner;
|
||||
use App\Models\Configuration;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
/**
|
||||
* Administrative competition-winner workflows (ported from CodeIgniter {@see \App\Controllers\Admin\CompetitionWinnersController}).
|
||||
*/
|
||||
class CompetitionWinnersAdminService
|
||||
{
|
||||
protected string $classStudentTable = '';
|
||||
|
||||
protected bool $hasClassWinnerTable = false;
|
||||
|
||||
protected bool $hasQuestionCount = false;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
if (Schema::hasTable('class_student')) {
|
||||
$this->classStudentTable = 'class_student';
|
||||
} elseif (Schema::hasTable('student_class')) {
|
||||
$this->classStudentTable = 'student_class';
|
||||
}
|
||||
|
||||
$this->hasClassWinnerTable = Schema::hasTable('competition_class_winners');
|
||||
$this->hasQuestionCount = $this->hasClassWinnerTable
|
||||
&& Schema::hasColumn('competition_class_winners', 'question_count');
|
||||
}
|
||||
|
||||
/** @return array<int, string> */
|
||||
public function getClassSectionMap(): array
|
||||
{
|
||||
$sections = ClassSection::query()
|
||||
->select('class_section_id', 'class_section_name')
|
||||
->orderBy('class_section_name', 'ASC')
|
||||
->get();
|
||||
|
||||
$map = [];
|
||||
foreach ($sections as $section) {
|
||||
$id = (int) ($section->class_section_id ?? 0);
|
||||
if ($id > 0) {
|
||||
$map[$id] = (string) ($section->class_section_name ?? $id);
|
||||
}
|
||||
}
|
||||
|
||||
return $map;
|
||||
}
|
||||
|
||||
public function getClassSectionName(int $classSectionId): ?string
|
||||
{
|
||||
if ($classSectionId <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$row = ClassSection::query()
|
||||
->where('class_section_id', $classSectionId)
|
||||
->value('class_section_name');
|
||||
|
||||
return $row !== null ? (string) $row : null;
|
||||
}
|
||||
|
||||
/** @return array<int, int> */
|
||||
public function getClassStudentCounts(?string $schoolYear): array
|
||||
{
|
||||
if ($this->classStudentTable === '') {
|
||||
return [];
|
||||
}
|
||||
|
||||
$q = DB::table($this->classStudentTable)
|
||||
->select('class_section_id', DB::raw('COUNT(*) as total'))
|
||||
->whereNotNull('class_section_id')
|
||||
->groupBy('class_section_id');
|
||||
|
||||
if ($schoolYear && Schema::hasColumn($this->classStudentTable, 'school_year')) {
|
||||
$q->where('school_year', $schoolYear);
|
||||
}
|
||||
|
||||
$map = [];
|
||||
foreach ($q->get() as $row) {
|
||||
$classId = (int) ($row->class_section_id ?? 0);
|
||||
if ($classId > 0) {
|
||||
$map[$classId] = (int) ($row->total ?? 0);
|
||||
}
|
||||
}
|
||||
|
||||
return $map;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{overrides: array<int,int>, questions: array<int,int>, prizes: array<int, array<int, float|null>>}
|
||||
*/
|
||||
public function getClassSettings(int $competitionId): array
|
||||
{
|
||||
if (! $this->hasClassWinnerTable) {
|
||||
return ['overrides' => [], 'questions' => [], 'prizes' => []];
|
||||
}
|
||||
|
||||
$rows = CompetitionClassWinner::query()
|
||||
->where('competition_id', $competitionId)
|
||||
->get();
|
||||
|
||||
$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 && $row->question_count !== null) {
|
||||
$questions[$classId] = (int) $row->question_count;
|
||||
}
|
||||
$prizes[$classId] = [
|
||||
1 => CompetitionWinnersDomain::castPrize($row->prize_1 ?? null),
|
||||
2 => CompetitionWinnersDomain::castPrize($row->prize_2 ?? null),
|
||||
3 => CompetitionWinnersDomain::castPrize($row->prize_3 ?? null),
|
||||
4 => CompetitionWinnersDomain::castPrize($row->prize_4 ?? null),
|
||||
5 => CompetitionWinnersDomain::castPrize($row->prize_5 ?? null),
|
||||
6 => CompetitionWinnersDomain::castPrize($row->prize_6 ?? null),
|
||||
];
|
||||
}
|
||||
|
||||
return ['overrides' => $overrides, 'questions' => $questions, 'prizes' => $prizes];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int|string, mixed> $overrides
|
||||
* @param array<int|string, mixed> $questionCounts
|
||||
* @param array<int|string, mixed> $prizes
|
||||
*/
|
||||
public function saveClassSettings(
|
||||
int $competitionId,
|
||||
array $overrides,
|
||||
array $questionCounts,
|
||||
array $prizes,
|
||||
?int $limitClassId = null
|
||||
): void {
|
||||
if (! $this->hasClassWinnerTable) {
|
||||
return;
|
||||
}
|
||||
|
||||
$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 = CompetitionClassWinner::query()
|
||||
->where('competition_id', $competitionId)
|
||||
->where('class_section_id', $classId)
|
||||
->first();
|
||||
|
||||
$prizeValues = CompetitionWinnersDomain::normalizePrizeInputs((array) ($prizes[$classId] ?? []));
|
||||
$hasPrize = array_filter($prizeValues, static fn ($v) => $v !== null);
|
||||
|
||||
if ($overrideValue === null && $questionValue === null && empty($hasPrize)) {
|
||||
if ($existing) {
|
||||
$existing->delete();
|
||||
}
|
||||
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) {
|
||||
$existing->fill($payload);
|
||||
$existing->save();
|
||||
} else {
|
||||
CompetitionClassWinner::query()->create($payload);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $competition
|
||||
* @return list<array<string, mixed>>
|
||||
*/
|
||||
public function getStudentsForCompetition(array $competition, int $classSectionId): array
|
||||
{
|
||||
if ($classSectionId <= 0 || $this->classStudentTable === '') {
|
||||
return [];
|
||||
}
|
||||
|
||||
$q = DB::table('students as s')
|
||||
->select('s.id', 's.school_id', 's.firstname', 's.lastname')
|
||||
->join($this->classStudentTable.' as cs', 'cs.student_id', '=', 's.id')
|
||||
->where('cs.class_section_id', $classSectionId);
|
||||
|
||||
if (Schema::hasColumn($this->classStudentTable, 'school_year') && ! empty($competition['school_year'])) {
|
||||
$q->where('cs.school_year', $competition['school_year']);
|
||||
}
|
||||
|
||||
return $q->distinct()->orderBy('s.lastname')->orderBy('s.firstname')->get()->map(fn ($r) => (array) $r)->all();
|
||||
}
|
||||
|
||||
/** @return array<int, int> */
|
||||
public function getQuestionCountsForCompetition(int $competitionId): array
|
||||
{
|
||||
if (! $this->hasQuestionCount || ! $this->hasClassWinnerTable) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$rows = CompetitionClassWinner::query()
|
||||
->where('competition_id', $competitionId)
|
||||
->whereNotNull('question_count')
|
||||
->get(['class_section_id', 'question_count']);
|
||||
|
||||
$out = [];
|
||||
foreach ($rows as $row) {
|
||||
$classId = (int) ($row->class_section_id ?? 0);
|
||||
if ($classId > 0 && $row->question_count !== null) {
|
||||
$out[$classId] = (int) $row->question_count;
|
||||
}
|
||||
}
|
||||
|
||||
return $out;
|
||||
}
|
||||
|
||||
/** @return array{competitions: \Illuminate\Support\Collection, sectionMap: array<int, string>} */
|
||||
public function indexData(): array
|
||||
{
|
||||
$competitions = Competition::query()->orderByDesc('id')->get();
|
||||
$sectionMap = $this->getClassSectionMap();
|
||||
|
||||
return ['competitions' => $competitions, 'sectionMap' => $sectionMap];
|
||||
}
|
||||
|
||||
/** @return array<string, mixed> */
|
||||
public function createFormData(): array
|
||||
{
|
||||
$schoolYear = Configuration::getConfig('school_year');
|
||||
$classCounts = $this->getClassStudentCounts($schoolYear);
|
||||
$classSections = ClassSection::query()->orderBy('class_section_name')->get()->map(fn ($r) => $r->toArray())->all();
|
||||
$classSections = CompetitionWinnersDomain::filterSectionsWithStudents($classSections, $classCounts);
|
||||
$classRows = CompetitionWinnersDomain::buildClassRows($classSections, $classCounts, [], [], []);
|
||||
|
||||
return [
|
||||
'mode' => 'create',
|
||||
'competition' => null,
|
||||
'classSections' => $classSections,
|
||||
'classRows' => $classRows,
|
||||
'defaultSemester' => Configuration::getConfig('semester'),
|
||||
'defaultSchoolYear' => $schoolYear,
|
||||
];
|
||||
}
|
||||
|
||||
/** @return array<string, mixed>|null */
|
||||
public function settingsFormData(int $id): ?array
|
||||
{
|
||||
$competition = Competition::query()->find($id);
|
||||
if (! $competition) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$row = $competition->toArray();
|
||||
$schoolYear = $row['school_year'] ?? Configuration::getConfig('school_year');
|
||||
$classCounts = $this->getClassStudentCounts($schoolYear);
|
||||
$classSections = ClassSection::query()->orderBy('class_section_name')->get()->map(fn ($r) => $r->toArray())->all();
|
||||
$classSections = CompetitionWinnersDomain::filterSectionsWithStudents($classSections, $classCounts);
|
||||
$settings = $this->getClassSettings((int) $id);
|
||||
$limitClassId = (int) ($row['class_section_id'] ?? 0);
|
||||
$classRows = CompetitionWinnersDomain::buildClassRows(
|
||||
$classSections,
|
||||
$classCounts,
|
||||
$settings['overrides'],
|
||||
$settings['questions'],
|
||||
$settings['prizes'],
|
||||
$limitClassId
|
||||
);
|
||||
|
||||
return [
|
||||
'mode' => 'edit',
|
||||
'competition' => $row,
|
||||
'classSections' => $classSections,
|
||||
'classRows' => $classRows,
|
||||
'defaultSemester' => Configuration::getConfig('semester'),
|
||||
'defaultSchoolYear' => $schoolYear,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $validated
|
||||
*/
|
||||
public function storeCompetition(array $validated, array $overrides, array $questionCounts, array $prizes, ?int $actorId): int
|
||||
{
|
||||
$classSectionId = $validated['class_section_id'] ?? null;
|
||||
$classSectionId = $classSectionId !== '' && $classSectionId !== null ? (int) $classSectionId : null;
|
||||
|
||||
$competition = Competition::query()->create([
|
||||
'title' => $validated['title'],
|
||||
'semester' => $validated['semester'] ?? null,
|
||||
'school_year' => $validated['school_year'] ?? null,
|
||||
'class_section_id' => $classSectionId,
|
||||
'start_date' => $validated['start_date'] ?? null,
|
||||
'end_date' => $validated['end_date'] ?? null,
|
||||
'created_by' => $actorId,
|
||||
]);
|
||||
|
||||
$this->saveClassSettings((int) $competition->id, $overrides, $questionCounts, $prizes, $classSectionId);
|
||||
|
||||
return (int) $competition->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $validated
|
||||
*/
|
||||
public function updateCompetition(int $id, array $validated, array $overrides, array $questionCounts, array $prizes): bool
|
||||
{
|
||||
$competition = Competition::query()->find($id);
|
||||
if (! $competition) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$classSectionId = $validated['class_section_id'] ?? null;
|
||||
$classSectionId = $classSectionId !== '' && $classSectionId !== null ? (int) $classSectionId : null;
|
||||
|
||||
$competition->update([
|
||||
'title' => $validated['title'],
|
||||
'semester' => $validated['semester'] ?? null,
|
||||
'school_year' => $validated['school_year'] ?? null,
|
||||
'class_section_id' => $classSectionId,
|
||||
'start_date' => $validated['start_date'] ?? null,
|
||||
'end_date' => $validated['end_date'] ?? null,
|
||||
]);
|
||||
|
||||
$this->saveClassSettings($id, $overrides, $questionCounts, $prizes, $classSectionId);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/** @return array<string, mixed>|null */
|
||||
public function editScoresData(int $id, ?int $queryClassSectionId): ?array
|
||||
{
|
||||
$competition = Competition::query()->find($id);
|
||||
if (! $competition) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$c = $competition->toArray();
|
||||
$lockedClassSectionId = (int) ($c['class_section_id'] ?? 0);
|
||||
$selectedClassSectionId = (int) ($queryClassSectionId ?? 0);
|
||||
$activeClassSectionId = $lockedClassSectionId > 0 ? $lockedClassSectionId : $selectedClassSectionId;
|
||||
|
||||
$students = [];
|
||||
$scoreMap = [];
|
||||
|
||||
if ($activeClassSectionId > 0) {
|
||||
$students = $this->getStudentsForCompetition($c, $activeClassSectionId);
|
||||
$existingScores = CompetitionScore::query()
|
||||
->where('competition_id', $id)
|
||||
->where('class_section_id', $activeClassSectionId)
|
||||
->get();
|
||||
|
||||
foreach ($existingScores as $row) {
|
||||
$scoreMap[(int) $row->student_id] = $row->score;
|
||||
}
|
||||
}
|
||||
|
||||
$schoolYear = $c['school_year'] ?? null;
|
||||
$classCounts = $this->getClassStudentCounts($schoolYear);
|
||||
$classSections = ClassSection::query()->orderBy('class_section_name')->get()->map(fn ($r) => $r->toArray())->all();
|
||||
$classSections = CompetitionWinnersDomain::filterSectionsWithStudents($classSections, $classCounts);
|
||||
$settings = $this->getClassSettings((int) $id);
|
||||
$classStudentCount = $classCounts[$activeClassSectionId] ?? 0;
|
||||
$classOverride = $settings['overrides'][$activeClassSectionId] ?? null;
|
||||
$classAuto = CompetitionWinnersDomain::winnersForCount($classStudentCount);
|
||||
$classFinal = $classOverride !== null ? $classOverride : $classAuto;
|
||||
$classQuestionCount = $settings['questions'][$activeClassSectionId] ?? null;
|
||||
|
||||
return [
|
||||
'competition' => $c,
|
||||
'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' => $settings['prizes'][$activeClassSectionId] ?? [],
|
||||
];
|
||||
}
|
||||
|
||||
/** @param array<int|string, mixed> $scores */
|
||||
public function saveScores(int $competitionId, array $scores, ?int $postClassSectionId, array $competitionRow): bool
|
||||
{
|
||||
$classSectionId = (int) ($competitionRow['class_section_id'] ?? 0);
|
||||
if ($classSectionId <= 0) {
|
||||
$classSectionId = (int) ($postClassSectionId ?? 0);
|
||||
}
|
||||
if ($classSectionId <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ($scores as $studentId => $scoreValue) {
|
||||
$scoreValue = trim((string) $scoreValue);
|
||||
if ($scoreValue === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$scoreFloat = (float) $scoreValue;
|
||||
|
||||
$existing = CompetitionScore::query()
|
||||
->where('competition_id', $competitionId)
|
||||
->where('student_id', (int) $studentId)
|
||||
->where('class_section_id', $classSectionId)
|
||||
->first();
|
||||
|
||||
if ($existing) {
|
||||
$existing->score = $scoreFloat;
|
||||
$existing->class_section_id = $classSectionId;
|
||||
$existing->save();
|
||||
} else {
|
||||
CompetitionScore::query()->create([
|
||||
'competition_id' => $competitionId,
|
||||
'student_id' => (int) $studentId,
|
||||
'class_section_id' => $classSectionId,
|
||||
'score' => $scoreFloat,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/** @return array<string, mixed>|null */
|
||||
public function previewData(int $id): ?array
|
||||
{
|
||||
$competition = Competition::query()->find($id);
|
||||
if (! $competition) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$c = $competition->toArray();
|
||||
|
||||
$rows = DB::table('competition_scores as cs')
|
||||
->leftJoin('students as s', 's.id', '=', 'cs.student_id')
|
||||
->select('cs.*', 's.firstname', 's.lastname')
|
||||
->where('cs.competition_id', $id)
|
||||
->get()
|
||||
->map(fn ($r) => (array) $r)
|
||||
->all();
|
||||
|
||||
$rankedByClass = CompetitionWinnersDomain::groupScoresByClass($rows);
|
||||
$scoreCounts = CompetitionWinnersDomain::getScoreCountsByClass($rankedByClass);
|
||||
|
||||
$classCounts = $this->getClassStudentCounts($c['school_year'] ?? null);
|
||||
$settings = $this->getClassSettings((int) $id);
|
||||
$winnersCountByClass = CompetitionWinnersDomain::getWinnersCountByClass(
|
||||
$c,
|
||||
$classCounts,
|
||||
$scoreCounts,
|
||||
$settings['overrides']
|
||||
);
|
||||
|
||||
$topByClass = CompetitionWinnersDomain::getTopWinnersByClass(
|
||||
$rankedByClass,
|
||||
$winnersCountByClass,
|
||||
$settings['prizes'],
|
||||
$settings['questions']
|
||||
);
|
||||
|
||||
return [
|
||||
'competition' => $c,
|
||||
'classMap' => $this->getClassSectionMap(),
|
||||
'classCounts' => $classCounts,
|
||||
'overrideMap' => $settings['overrides'],
|
||||
'questionCounts' => $settings['questions'],
|
||||
'prizeMap' => $settings['prizes'],
|
||||
'winnersCountByClass' => $winnersCountByClass,
|
||||
'rankedByClass' => $rankedByClass,
|
||||
'topByClass' => $topByClass,
|
||||
];
|
||||
}
|
||||
|
||||
/** @return list<array<string,mixed>> */
|
||||
public function winnersListingRows(int $competitionId): array
|
||||
{
|
||||
return DB::table('competition_winners as cw')
|
||||
->selectRaw('cw.*, s.firstname, s.lastname, s.school_id, s.photo_consent, cs.class_section_name')
|
||||
->leftJoin('students as s', 's.id', '=', 'cw.student_id')
|
||||
->leftJoin('classSection as cs', 'cs.class_section_id', '=', 'cw.class_section_id')
|
||||
->where('cw.competition_id', $competitionId)
|
||||
->orderBy('cw.class_section_id')
|
||||
->orderBy('cw.rank')
|
||||
->get()
|
||||
->map(fn ($r) => (array) $r)
|
||||
->all();
|
||||
}
|
||||
|
||||
public function publishWinners(int $id): bool
|
||||
{
|
||||
$competition = Competition::query()->find($id);
|
||||
if (! $competition) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$c = $competition->toArray();
|
||||
|
||||
$rows = CompetitionScore::query()->where('competition_id', $id)->get()->map(fn ($r) => $r->toArray())->all();
|
||||
|
||||
$rankedByClass = CompetitionWinnersDomain::groupScoresByClass($rows);
|
||||
$scoreCounts = CompetitionWinnersDomain::getScoreCountsByClass($rankedByClass);
|
||||
$classCounts = $this->getClassStudentCounts($c['school_year'] ?? null);
|
||||
$settings = $this->getClassSettings((int) $id);
|
||||
$winnersCountByClass = CompetitionWinnersDomain::getWinnersCountByClass(
|
||||
$c,
|
||||
$classCounts,
|
||||
$scoreCounts,
|
||||
$settings['overrides']
|
||||
);
|
||||
|
||||
CompetitionWinner::query()->where('competition_id', $id)->delete();
|
||||
|
||||
foreach ($rankedByClass as $classSectionId => $rowsForClass) {
|
||||
$limit = (int) ($winnersCountByClass[$classSectionId] ?? 0);
|
||||
if ($limit <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$topWithPrizes = CompetitionWinnersDomain::assignPrizesByScore(
|
||||
$rowsForClass,
|
||||
$settings['prizes'][$classSectionId] ?? [],
|
||||
$limit,
|
||||
$settings['questions'][$classSectionId] ?? null
|
||||
);
|
||||
|
||||
foreach ($topWithPrizes as $row) {
|
||||
CompetitionWinner::query()->create([
|
||||
'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' => now()->toDateTimeString(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
$competition->update([
|
||||
'is_published' => 1,
|
||||
'published_at' => now()->toDateTimeString(),
|
||||
]);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function lockCompetition(int $id, ?int $userId): bool
|
||||
{
|
||||
$competition = Competition::query()->find($id);
|
||||
if (! $competition) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($competition->is_locked) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$competition->update([
|
||||
'is_locked' => 1,
|
||||
'locked_at' => now()->toDateTimeString(),
|
||||
'locked_by' => $userId,
|
||||
]);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function unlockCompetition(int $id): bool
|
||||
{
|
||||
$competition = Competition::query()->find($id);
|
||||
if (! $competition) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (! $competition->is_locked) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$competition->update([
|
||||
'is_locked' => 0,
|
||||
'locked_at' => null,
|
||||
'locked_by' => null,
|
||||
]);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Export competition scores into quiz rows (SQL preserved from CodeIgniter).
|
||||
*
|
||||
* @return array{ok: bool, message: string, quizIndexes?: array<int,int>}
|
||||
*/
|
||||
public function exportCompetitionToQuiz(int $competitionId, int $classSectionId, ?int $schoolId, ?int $userId, ?string $semester, ?string $schoolYear): array
|
||||
{
|
||||
if ($competitionId <= 0) {
|
||||
return ['ok' => false, 'message' => 'Competition not found.'];
|
||||
}
|
||||
|
||||
$competition = Competition::query()->find($competitionId);
|
||||
if (! $competition) {
|
||||
return ['ok' => false, 'message' => 'Competition not found.'];
|
||||
}
|
||||
|
||||
$lockedClassSectionId = (int) ($competition->class_section_id ?? 0);
|
||||
if ($lockedClassSectionId > 0) {
|
||||
$classSectionId = $lockedClassSectionId;
|
||||
}
|
||||
|
||||
if ($classSectionId > 0 && $lockedClassSectionId > 0 && $lockedClassSectionId !== $classSectionId) {
|
||||
return ['ok' => false, 'message' => 'This competition is locked to a different class section.'];
|
||||
}
|
||||
|
||||
$semester = $semester ?: Configuration::getConfig('semester');
|
||||
$schoolYear = $schoolYear ?: Configuration::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();
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$cid = (int) ($row->class_section_id ?? 0);
|
||||
if ($cid > 0) {
|
||||
$classSectionIds[] = $cid;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($classSectionIds === []) {
|
||||
return ['ok' => false, 'message' => 'No class sections with scores found for this competition.'];
|
||||
}
|
||||
|
||||
$quizIndexes = [];
|
||||
|
||||
DB::transaction(function () use ($competitionId, $classSectionIds, $schoolId, $userId, $semester, $schoolYear, &$quizIndexes): void {
|
||||
foreach ($classSectionIds as $cid) {
|
||||
$cid = (int) $cid;
|
||||
if ($cid <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$row = DB::selectOne(
|
||||
'SELECT COALESCE(MAX(quiz_index), 0) + 1 AS next_index
|
||||
FROM quiz
|
||||
WHERE class_section_id = ?
|
||||
AND semester = ?
|
||||
AND school_year = ?',
|
||||
[$cid, $semester, $schoolYear]
|
||||
);
|
||||
|
||||
$quizIndex = (int) ($row->next_index ?? 1);
|
||||
|
||||
$sql = <<<'SQL'
|
||||
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 = ?
|
||||
SQL;
|
||||
|
||||
DB::statement($sql, [
|
||||
$schoolId,
|
||||
$userId,
|
||||
$quizIndex,
|
||||
$semester,
|
||||
$schoolYear,
|
||||
$competitionId,
|
||||
$cid,
|
||||
]);
|
||||
|
||||
$quizIndexes[$cid] = $quizIndex;
|
||||
}
|
||||
});
|
||||
|
||||
if ($quizIndexes === []) {
|
||||
return ['ok' => false, 'message' => 'No scores were exported.'];
|
||||
}
|
||||
|
||||
return ['ok' => true, 'message' => 'Exported.', 'quizIndexes' => $quizIndexes];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,345 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Admin\CompetitionWinners;
|
||||
|
||||
/** Pure domain logic ported from CodeIgniter `Admin\CompetitionWinnersController` (private helpers). */
|
||||
final class CompetitionWinnersDomain
|
||||
{
|
||||
/** @var array<int, array{min:int, max:?int, winners:int}> */
|
||||
private const WINNER_TIERS = [
|
||||
['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 static function winnersForCount(int $count): int
|
||||
{
|
||||
if ($count <= 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
foreach (self::WINNER_TIERS 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, array<string, mixed>> $classSections
|
||||
* @param array<int, int> $classCounts
|
||||
* @param array<int, int> $overrides
|
||||
* @param array<int, int|null> $questionCounts
|
||||
* @param array<int, array<int, float|null>> $prizeMap
|
||||
* @return list<array<string, mixed>>
|
||||
*/
|
||||
public static 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 = self::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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, array<string, mixed>> $classSections
|
||||
* @param array<int, int> $classCounts
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
public static 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;
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $competition Row including optional class_section_id
|
||||
* @param array<int, int> $classCounts
|
||||
* @param array<int, int> $scoreCounts
|
||||
* @param array<int, int> $overrides
|
||||
* @return array<int, int>
|
||||
*/
|
||||
public static 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 = self::winnersForCount((int) $baseCount);
|
||||
$minWinners = 3;
|
||||
$map[$classId] = $maxWinners >= $minWinners ? $maxWinners : $minWinners;
|
||||
}
|
||||
|
||||
return $map;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, array<string, mixed>> $rows
|
||||
* @return array<int, list<array<string, mixed>>>
|
||||
*/
|
||||
public static 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, list<array<string, mixed>>> $rankedByClass
|
||||
* @return array<int, int>
|
||||
*/
|
||||
public static function getScoreCountsByClass(array $rankedByClass): array
|
||||
{
|
||||
$counts = [];
|
||||
foreach ($rankedByClass as $classId => $rows) {
|
||||
$counts[(int) $classId] = count($rows);
|
||||
}
|
||||
|
||||
return $counts;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, list<array<string, mixed>>> $rankedByClass
|
||||
* @param array<int, int> $winnersCountByClass
|
||||
* @param array<int, array<int, float|null>> $prizeMap
|
||||
* @param array<int, int|null> $questionCounts
|
||||
* @return array<int, list<array<string, mixed>>>
|
||||
*/
|
||||
public static function getTopWinnersByClass(
|
||||
array $rankedByClass,
|
||||
array $winnersCountByClass,
|
||||
array $prizeMap,
|
||||
array $questionCounts
|
||||
): array {
|
||||
$out = [];
|
||||
|
||||
foreach ($rankedByClass as $classSectionId => $rows) {
|
||||
$limit = (int) ($winnersCountByClass[$classSectionId] ?? 0);
|
||||
if ($limit <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$out[$classSectionId] = self::assignPrizesByScore(
|
||||
$rows,
|
||||
$prizeMap[$classSectionId] ?? [],
|
||||
$limit,
|
||||
$questionCounts[$classSectionId] ?? null
|
||||
);
|
||||
}
|
||||
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, mixed> $prizes
|
||||
* @return array<int, float|null>
|
||||
*/
|
||||
public static 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;
|
||||
}
|
||||
|
||||
public static function castPrize(mixed $value): ?float
|
||||
{
|
||||
if ($value === null || $value === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (float) $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, float|null> $classPrizes
|
||||
*/
|
||||
public static function getPrizeForRank(array $classPrizes, int $rank): float
|
||||
{
|
||||
$val = $classPrizes[$rank] ?? null;
|
||||
|
||||
return $val !== null ? (float) $val : 0.0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<array<string, mixed>> $rows
|
||||
* @param array<int, float|null> $classPrizes
|
||||
* @return list<array<string, mixed>>
|
||||
*/
|
||||
public static 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 = self::getPrizeForRank($classPrizes, 1);
|
||||
}
|
||||
}
|
||||
|
||||
$out = [];
|
||||
foreach ($top as $row) {
|
||||
if ($sharedPrize !== null) {
|
||||
$prize = $sharedPrize;
|
||||
} else {
|
||||
$prize = self::getPrizeForRank($classPrizes, (int) ($row['rank'] ?? 1));
|
||||
}
|
||||
|
||||
$row['prize_amount'] = $prize;
|
||||
$out[] = $row;
|
||||
}
|
||||
|
||||
return $out;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user