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 */ 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 */ 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, questions: array, prizes: array>} */ 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 $overrides * @param array $questionCounts * @param array $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 $competition * @return list> */ 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 */ 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: Collection, sectionMap: array} */ public function indexData(): array { $competitions = Competition::query()->orderByDesc('id')->get(); $sectionMap = $this->getClassSectionMap(); return ['competitions' => $competitions, 'sectionMap' => $sectionMap]; } /** @return array */ 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|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 $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 $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|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 $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|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> */ 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 legacy). * * @return array{ok: bool, message: string, quizIndexes?: array} */ 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]; } }