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->Rect($x, $y, $w, $h);
$pdf->SetXY($x + $pad, $y + 3); $pdf->SetXY($x + $pad, $y + 3);
$pdf->SetFont('Helvetica', 'B', 11); $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->SetFont('Helvetica', '', 12);
$pdf->SetXY($x + 2 + $labelWidth, $y + 3); $pdf->SetXY($x + 2 + $labelWidth, $y + 3);
$pdf->Write(5, $rankValue); $pdf->Write(5, $rankValue);
@@ -1732,161 +1732,200 @@ $scoresEndY = $pdf->GetY();
]; ];
} }
private function calculateTermRanking( private function calculateTermRanking(
int $studentId, int $studentId,
int $sectionCode, int $sectionCode,
?int $sectionId, ?int $sectionId,
string $schoolYear, string $schoolYear,
?string $semester, ?string $semester,
?float $studentScore ?float $studentScore
): ?array { ): ?array {
if ($studentId <= 0 || $schoolYear === '' || $studentScore === null) { if ($studentId <= 0 || $schoolYear === '' || $studentScore === null) {
return 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([ $scoreVal = $row['semester_score'] ?? null;
$sectionCode > 0 ? $sectionCode : null,
$sectionId && $sectionId > 0 ? $sectionId : null,
])));
if (empty($sectionIds)) { if (!is_numeric($scoreVal)) {
return null; continue;
} }
$semesterForRank = trim((string)$semester); $rawScore = (float)$scoreVal;
$rankByFinalScore = $this->normalizeSemester($semesterForRank) === 'spring';
$builder = $this->db->table('semester_scores ss') $scoresByStudent[$sid] = [
->select('ss.student_id, ss.semester, ss.semester_score, ss.updated_at, ss.id, s.firstname, s.lastname') 'student_id' => $sid,
->join('students s', 's.id = ss.student_id', 'inner') 'score' => $rawScore,
->where('s.is_active', 1) '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) ->where('ss.school_year', $schoolYear)
->whereIn('ss.student_id', $studentIds)
->whereIn('ss.class_section_id', $sectionIds) ->whereIn('ss.class_section_id', $sectionIds)
->orderBy('ss.updated_at', 'DESC') ->orderBy('ss.updated_at', 'DESC')
->orderBy('ss.id', 'DESC'); ->orderBy('ss.id', 'DESC');
if ($semesterForRank !== '') { if ($semesterForRank !== '') {
$this->applySemesterFilter($builder, $semesterForRank, 'ss.semester'); $this->applySemesterExclusion($firstRowsBuilder, $semesterForRank, 'ss.semester');
} }
$rows = $builder->get()->getResultArray(); $firstRows = $firstRowsBuilder->get()->getResultArray();
if (empty($rows)) {
return null;
}
$scoresByStudent = []; $firstScoresByStudent = [];
$studentIds = [];
foreach ($rows as $row) { foreach ($firstRows as $row) {
$sid = (int)($row['student_id'] ?? 0); $sid = (int)($row['student_id'] ?? 0);
if ($sid <= 0 || isset($scoresByStudent[$sid])) {
if ($sid <= 0 || isset($firstScoresByStudent[$sid])) {
continue; continue;
} }
$scoreVal = $row['semester_score'] ?? null; $scoreVal = $row['semester_score'] ?? null;
if (!is_numeric($scoreVal)) { if (!is_numeric($scoreVal)) {
continue; continue;
} }
$scoresByStudent[$sid] = [ $firstScoresByStudent[$sid] = (float)$scoreVal;
'student_id' => $sid,
'score' => round((float)$scoreVal, 4),
'firstname' => trim((string)($row['firstname'] ?? '')),
'lastname' => trim((string)($row['lastname'] ?? '')),
];
$studentIds[] = $sid;
} }
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])) { if (empty($scoresByStudent) || !isset($scoresByStudent[$studentId])) {
return null; return null;
} }
if ($rankByFinalScore) { // Force the selected student to use the exact score already computed for the report.
$firstRowsBuilder = $this->db->table('semester_scores ss') // Then rank using the displayed precision: one decimal.
->select('ss.student_id, ss.semester_score, ss.updated_at, ss.id') $scoresByStudent[$studentId]['score'] = (float)$studentScore;
->where('ss.school_year', $schoolYear) $scoresByStudent[$studentId]['rank_score'] = round((float)$studentScore, 1);
->whereIn('ss.class_section_id', $sectionIds) } else {
->whereIn('ss.student_id', $studentIds) // Fall: also force selected student to match the report-card computed score.
->orderBy('ss.updated_at', 'DESC') $scoresByStudent[$studentId]['score'] = (float)$studentScore;
->orderBy('ss.id', 'DESC'); $scoresByStudent[$studentId]['rank_score'] = round((float)$studentScore, 1);
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;
} }
$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 private function formatOrdinal(?int $value): string
{ {
$n = (int)$value; $n = (int)$value;