This commit is contained in:
root
2026-05-30 03:29:48 -04:00
parent 9ee75fe4cc
commit 079c869477
+164 -125
View File
@@ -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;