From 079c869477f007cc25ae52c36dafc3c3edd4d908 Mon Sep 17 00:00:00 2001 From: root Date: Sat, 30 May 2026 03:29:48 -0400 Subject: [PATCH] fix ties --- .../View/ReportCardsController.php | 289 ++++++++++-------- 1 file changed, 164 insertions(+), 125 deletions(-) diff --git a/app/Controllers/View/ReportCardsController.php b/app/Controllers/View/ReportCardsController.php index be89614..cdba8d2 100644 --- a/app/Controllers/View/ReportCardsController.php +++ b/app/Controllers/View/ReportCardsController.php @@ -1014,9 +1014,9 @@ $drawRankCell = static function ( $pdf->Rect($x, $y, $w, $h); $pdf->SetXY($x + $pad, $y + 3); $pdf->SetFont('Helvetica', 'B', 11); - $pdf->Write(5, ' Rank: '); + $pdf->Write(5, ' Class Rank: '); - $labelWidth = $pdf->GetStringWidth(' Rank: '); + $labelWidth = $pdf->GetStringWidth(' Class Rank: '); $pdf->SetFont('Helvetica', '', 12); $pdf->SetXY($x + 2 + $labelWidth, $y + 3); $pdf->Write(5, $rankValue); @@ -1732,161 +1732,200 @@ $scoresEndY = $pdf->GetY(); ]; } - private function calculateTermRanking( - int $studentId, - int $sectionCode, - ?int $sectionId, - string $schoolYear, - ?string $semester, - ?float $studentScore - ): ?array { - if ($studentId <= 0 || $schoolYear === '' || $studentScore === null) { - return null; +private function calculateTermRanking( + int $studentId, + int $sectionCode, + ?int $sectionId, + string $schoolYear, + ?string $semester, + ?float $studentScore +): ?array { + if ($studentId <= 0 || $schoolYear === '' || $studentScore === null) { + return null; + } + + $sectionIds = array_values(array_unique(array_filter([ + $sectionCode > 0 ? $sectionCode : null, + $sectionId && $sectionId > 0 ? $sectionId : null, + ]))); + + if (empty($sectionIds)) { + return null; + } + + $semesterForRank = trim((string)$semester); + $rankByFinalScore = $this->normalizeSemester($semesterForRank) === 'spring'; + + $builder = $this->db->table('semester_scores ss') + ->select('ss.student_id, ss.semester, ss.semester_score, ss.updated_at, ss.id, s.firstname, s.lastname') + ->join('students s', 's.id = ss.student_id', 'inner') + ->where('s.is_active', 1) + ->where('ss.school_year', $schoolYear) + ->whereIn('ss.class_section_id', $sectionIds) + ->orderBy('ss.updated_at', 'DESC') + ->orderBy('ss.id', 'DESC'); + + if ($semesterForRank !== '') { + $this->applySemesterFilter($builder, $semesterForRank, 'ss.semester'); + } + + $rows = $builder->get()->getResultArray(); + + if (empty($rows)) { + return null; + } + + $scoresByStudent = []; + + foreach ($rows as $row) { + $sid = (int)($row['student_id'] ?? 0); + + if ($sid <= 0 || isset($scoresByStudent[$sid])) { + continue; } - $sectionIds = array_values(array_unique(array_filter([ - $sectionCode > 0 ? $sectionCode : null, - $sectionId && $sectionId > 0 ? $sectionId : null, - ]))); + $scoreVal = $row['semester_score'] ?? null; - if (empty($sectionIds)) { - return null; + if (!is_numeric($scoreVal)) { + continue; } - $semesterForRank = trim((string)$semester); - $rankByFinalScore = $this->normalizeSemester($semesterForRank) === 'spring'; + $rawScore = (float)$scoreVal; - $builder = $this->db->table('semester_scores ss') - ->select('ss.student_id, ss.semester, ss.semester_score, ss.updated_at, ss.id, s.firstname, s.lastname') - ->join('students s', 's.id = ss.student_id', 'inner') - ->where('s.is_active', 1) + $scoresByStudent[$sid] = [ + 'student_id' => $sid, + 'score' => $rawScore, + 'rank_score' => round($rawScore, 1), + 'firstname' => trim((string)($row['firstname'] ?? '')), + 'lastname' => trim((string)($row['lastname'] ?? '')), + ]; + } + + if (empty($scoresByStudent) || !isset($scoresByStudent[$studentId])) { + return null; + } + + // For Spring, rank by final year average: + // (first semester score + second semester score) / 2. + if ($rankByFinalScore) { + $studentIds = array_keys($scoresByStudent); + + $firstRowsBuilder = $this->db->table('semester_scores ss') + ->select('ss.student_id, ss.semester, ss.semester_score, ss.updated_at, ss.id') ->where('ss.school_year', $schoolYear) + ->whereIn('ss.student_id', $studentIds) ->whereIn('ss.class_section_id', $sectionIds) ->orderBy('ss.updated_at', 'DESC') ->orderBy('ss.id', 'DESC'); if ($semesterForRank !== '') { - $this->applySemesterFilter($builder, $semesterForRank, 'ss.semester'); + $this->applySemesterExclusion($firstRowsBuilder, $semesterForRank, 'ss.semester'); } - $rows = $builder->get()->getResultArray(); - if (empty($rows)) { - return null; - } + $firstRows = $firstRowsBuilder->get()->getResultArray(); - $scoresByStudent = []; - $studentIds = []; - foreach ($rows as $row) { + $firstScoresByStudent = []; + + foreach ($firstRows as $row) { $sid = (int)($row['student_id'] ?? 0); - if ($sid <= 0 || isset($scoresByStudent[$sid])) { + + if ($sid <= 0 || isset($firstScoresByStudent[$sid])) { continue; } $scoreVal = $row['semester_score'] ?? null; + if (!is_numeric($scoreVal)) { continue; } - $scoresByStudent[$sid] = [ - 'student_id' => $sid, - 'score' => round((float)$scoreVal, 4), - 'firstname' => trim((string)($row['firstname'] ?? '')), - 'lastname' => trim((string)($row['lastname'] ?? '')), - ]; - $studentIds[] = $sid; + $firstScoresByStudent[$sid] = (float)$scoreVal; } + foreach ($scoresByStudent as $sid => &$rankRow) { + if (!isset($firstScoresByStudent[$sid])) { + unset($scoresByStudent[$sid]); + continue; + } + + $avg = ((float)$firstScoresByStudent[$sid] + (float)$rankRow['score']) / 2; + + $rankRow['score'] = $avg; + $rankRow['rank_score'] = round($avg, 1); + } + + unset($rankRow); + if (empty($scoresByStudent) || !isset($scoresByStudent[$studentId])) { return null; } - if ($rankByFinalScore) { - $firstRowsBuilder = $this->db->table('semester_scores ss') - ->select('ss.student_id, ss.semester_score, ss.updated_at, ss.id') - ->where('ss.school_year', $schoolYear) - ->whereIn('ss.class_section_id', $sectionIds) - ->whereIn('ss.student_id', $studentIds) - ->orderBy('ss.updated_at', 'DESC') - ->orderBy('ss.id', 'DESC'); - - if ($semesterForRank !== '') { - $this->applySemesterExclusion($firstRowsBuilder, $semesterForRank, 'ss.semester'); - } - - $firstRows = $firstRowsBuilder->get()->getResultArray(); - $firstScoresByStudent = []; - foreach ($firstRows as $row) { - $sid = (int)($row['student_id'] ?? 0); - if ($sid <= 0 || isset($firstScoresByStudent[$sid])) { - continue; - } - - $scoreVal = $row['semester_score'] ?? null; - if (!is_numeric($scoreVal)) { - continue; - } - - $firstScoresByStudent[$sid] = (float)$scoreVal; - } - - foreach ($scoresByStudent as $sid => &$rankRow) { - if (!isset($firstScoresByStudent[$sid])) { - unset($scoresByStudent[$sid]); - continue; - } - - $rankRow['score'] = round(((float)$firstScoresByStudent[$sid] + (float)$rankRow['score']) / 2, 4); - } - unset($rankRow); - - if (empty($scoresByStudent) || !isset($scoresByStudent[$studentId])) { - return null; - } - - $scoresByStudent[$studentId]['score'] = round((float)$studentScore, 4); - } - - $rankable = array_values($scoresByStudent); - usort($rankable, static function (array $a, array $b): int { - $scoreCmp = $b['score'] <=> $a['score']; - if ($scoreCmp !== 0) { - return $scoreCmp; - } - - $lastCmp = strcasecmp($a['lastname'], $b['lastname']); - if ($lastCmp !== 0) { - return $lastCmp; - } - - $firstCmp = strcasecmp($a['firstname'], $b['firstname']); - if ($firstCmp !== 0) { - return $firstCmp; - } - - return $a['student_id'] <=> $b['student_id']; - }); - - $position = null; - $previousScore = null; - foreach ($rankable as $index => $row) { - if ($previousScore === null || abs($row['score'] - $previousScore) > 0.0001) { - $position = $index + 1; - $previousScore = $row['score']; - } - - if ((int)$row['student_id'] === $studentId) { - $total = count($rankable); - return [ - 'position' => $position, - 'total' => $total, - 'display' => $this->formatOrdinal($position) . ' out of ' . $total, - ]; - } - } - - return null; + // Force the selected student to use the exact score already computed for the report. + // Then rank using the displayed precision: one decimal. + $scoresByStudent[$studentId]['score'] = (float)$studentScore; + $scoresByStudent[$studentId]['rank_score'] = round((float)$studentScore, 1); + } else { + // Fall: also force selected student to match the report-card computed score. + $scoresByStudent[$studentId]['score'] = (float)$studentScore; + $scoresByStudent[$studentId]['rank_score'] = round((float)$studentScore, 1); } + $rankable = array_values($scoresByStudent); + + usort($rankable, static function (array $a, array $b): int { + // Highest displayed/rank score first. + $scoreCmp = $b['rank_score'] <=> $a['rank_score']; + + if ($scoreCmp !== 0) { + return $scoreCmp; + } + + // Tie-breakers only control display order. + // They do NOT change rank. + $lastCmp = strcasecmp($a['lastname'], $b['lastname']); + + if ($lastCmp !== 0) { + return $lastCmp; + } + + $firstCmp = strcasecmp($a['firstname'], $b['firstname']); + + if ($firstCmp !== 0) { + return $firstCmp; + } + + return $a['student_id'] <=> $b['student_id']; + }); + + $position = null; + $previousScore = null; + + foreach ($rankable as $index => $row) { + $currentScore = (float)$row['rank_score']; + + // Competition ranking: + // 1, 1, 3, 4... + // Same score = same rank. + if ($previousScore === null || abs($currentScore - $previousScore) > 0.0001) { + $position = $index + 1; + $previousScore = $currentScore; + } + + if ((int)$row['student_id'] === $studentId) { + $total = count($rankable); + + return [ + 'position' => $position, + 'total' => $total, + 'score' => $currentScore, + 'display' => $this->formatOrdinal($position) . ' out of ' . $total, + ]; + } + } + + return null; +} + private function formatOrdinal(?int $value): string { $n = (int)$value;