Compare commits
3 Commits
9ee75fe4cc
...
5.0.6
| Author | SHA1 | Date | |
|---|---|---|---|
| f24f4311e8 | |||
| 89913d7473 | |||
| 079c869477 |
@@ -1068,37 +1068,67 @@ class GradingController extends Controller
|
|||||||
return $scores;
|
return $scores;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function belowSixty()
|
public function belowSixty()
|
||||||
{
|
{
|
||||||
$configuredYear = (string) $this->schoolYear;
|
$configuredYear = (string) $this->schoolYear;
|
||||||
|
|
||||||
$requestedSemester = strtolower(trim((string)($this->request->getGet('semester') ?? '')));
|
$schoolYear = trim((string)($this->request->getGet('school_year') ?? ''));
|
||||||
$requestedYear = trim((string)($this->request->getGet('school_year') ?? ''));
|
|
||||||
|
|
||||||
// This page intentionally supports only Fall and Whole Year.
|
if ($schoolYear === '') {
|
||||||
// Spring is still used internally for the Whole Year calculation, but it is not selectable here.
|
$schoolYear = $configuredYear;
|
||||||
$isYearMode = ($requestedSemester === 'year');
|
|
||||||
$semester = $isYearMode ? 'year' : 'Fall';
|
|
||||||
$schoolYear = $requestedYear !== '' ? $requestedYear : $configuredYear;
|
|
||||||
|
|
||||||
$schoolYears = $this->getSchoolYearsForScores($schoolYear);
|
|
||||||
$rows = $this->fetchBelowSixtyRows($schoolYear, $semester);
|
|
||||||
|
|
||||||
$canViewGrading = $this->userHasMenuUrl('grading');
|
|
||||||
|
|
||||||
return view('grading/below_sixty', [
|
|
||||||
'rows' => $rows,
|
|
||||||
'semester' => $semester,
|
|
||||||
'schoolYear' => $schoolYear,
|
|
||||||
'schoolYears' => $schoolYears,
|
|
||||||
'canViewGrading' => $canViewGrading,
|
|
||||||
'isYearMode' => $isYearMode,
|
|
||||||
'semesterOptions' => ['Fall'],
|
|
||||||
'showAllSemesterOption' => true,
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This page is Fall only.
|
||||||
|
$semester = 'fall';
|
||||||
|
$isYearMode = false;
|
||||||
|
|
||||||
|
$schoolYears = $this->getSchoolYearsForScores($schoolYear);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Use your existing below-60 fetcher.
|
||||||
|
* Do NOT query below_sixty_status. That table does not exist.
|
||||||
|
*/
|
||||||
|
$rows = $this->fetchBelowSixtyRows($schoolYear, $semester);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Hard guard:
|
||||||
|
* Keep only Fall semester rows with semester_score < 60.
|
||||||
|
* This prevents whole-year rows or accidental other semester rows
|
||||||
|
* from sneaking into this Fall-only page.
|
||||||
|
*/
|
||||||
|
$rows = array_values(array_filter($rows, static function ($row) {
|
||||||
|
$semesterValue = strtolower(trim((string)($row['semester'] ?? 'fall')));
|
||||||
|
$scoreRaw = $row['semester_score'] ?? null;
|
||||||
|
|
||||||
|
if ($semesterValue !== '' && $semesterValue !== 'fall') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!is_numeric($scoreRaw)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (float)$scoreRaw < 60;
|
||||||
|
}));
|
||||||
|
|
||||||
|
foreach ($rows as &$row) {
|
||||||
|
$row['status'] = $row['status'] ?? 'Open';
|
||||||
|
$row['note'] = $row['note'] ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
unset($row);
|
||||||
|
|
||||||
|
$canViewGrading = $this->userHasMenuUrl('grading');
|
||||||
|
|
||||||
|
return view('grading/below_sixty', [
|
||||||
|
'rows' => $rows,
|
||||||
|
'semester' => $semester,
|
||||||
|
'schoolYear' => $schoolYear,
|
||||||
|
'schoolYears' => $schoolYears,
|
||||||
|
'isYearMode' => $isYearMode,
|
||||||
|
'canViewGrading' => $canViewGrading,
|
||||||
|
]);
|
||||||
|
}
|
||||||
public function editBelowSixtyEmail()
|
public function editBelowSixtyEmail()
|
||||||
{
|
{
|
||||||
$studentId = (int)$this->request->getGet('student_id');
|
$studentId = (int)$this->request->getGet('student_id');
|
||||||
@@ -2298,87 +2328,185 @@ class GradingController extends Controller
|
|||||||
return 50000;
|
return 50000;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function belowSixtyDecisions()
|
public function belowSixtyDecisions()
|
||||||
{
|
{
|
||||||
$configuredSemester = (string) $this->semester;
|
$configuredYear = (string) $this->schoolYear;
|
||||||
$configuredYear = (string) $this->schoolYear;
|
|
||||||
|
|
||||||
$semester = trim((string)($this->request->getGet('semester') ?? ''));
|
|
||||||
$schoolYear = trim((string)($this->request->getGet('school_year') ?? ''));
|
$schoolYear = trim((string)($this->request->getGet('school_year') ?? ''));
|
||||||
|
|
||||||
if ($semester === '') {
|
|
||||||
$semester = $configuredSemester !== '' ? $configuredSemester : 'Fall';
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($schoolYear === '') {
|
if ($schoolYear === '') {
|
||||||
$schoolYear = $configuredYear;
|
$schoolYear = $configuredYear;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This page is whole-year only.
|
||||||
|
$semester = 'year';
|
||||||
|
|
||||||
$schoolYears = $this->getSchoolYearsForScores($schoolYear);
|
$schoolYears = $this->getSchoolYearsForScores($schoolYear);
|
||||||
$rows = $this->fetchBelowSixtyRows($schoolYear, $semester);
|
|
||||||
|
|
||||||
$studentIds = array_values(array_unique(array_filter(
|
$db = $this->db;
|
||||||
array_map(static fn($r) => (int)($r['student_id'] ?? 0), $rows),
|
|
||||||
static fn($id) => $id > 0
|
|
||||||
)));
|
|
||||||
|
|
||||||
// ── Load manual below-60 semester decisions ─────────────────────────────
|
/*
|
||||||
//
|
* Whole-year score source:
|
||||||
// This table still uses semester, because below-60 decisions are tied to
|
* Fall semester_score + Spring semester_score / 2
|
||||||
// the selected below-60 screen/term.
|
*
|
||||||
$decisionModel = new BelowSixtyDecisionModel();
|
* Only students with BOTH Fall and Spring scores are included.
|
||||||
|
* Only students with year_score < 60 are listed.
|
||||||
|
*/
|
||||||
|
$scoreRows = $db->table('semester_scores ss')
|
||||||
|
->select([
|
||||||
|
's.id AS student_id',
|
||||||
|
's.firstname',
|
||||||
|
's.lastname',
|
||||||
|
's.school_id',
|
||||||
|
'cs.class_section_name',
|
||||||
|
'LOWER(TRIM(ss.semester)) AS sem_key',
|
||||||
|
'ss.semester_score',
|
||||||
|
])
|
||||||
|
->join('students s', 's.id = ss.student_id', 'inner')
|
||||||
|
->join('classSection cs', 'cs.class_section_id = ss.class_section_id', 'left')
|
||||||
|
->where('s.is_active', 1)
|
||||||
|
->where('ss.school_year', $schoolYear)
|
||||||
|
->whereIn('LOWER(TRIM(ss.semester))', ['fall', 'spring'])
|
||||||
|
->where('ss.semester_score IS NOT NULL', null, false)
|
||||||
|
->orderBy('cs.class_section_name', 'ASC')
|
||||||
|
->orderBy('s.lastname', 'ASC')
|
||||||
|
->orderBy('s.firstname', 'ASC')
|
||||||
|
->get()
|
||||||
|
->getResultArray();
|
||||||
|
|
||||||
$decisionMap = [];
|
$studentMap = [];
|
||||||
|
|
||||||
if (!empty($studentIds)) {
|
foreach ($scoreRows as $row) {
|
||||||
$dRows = $decisionModel
|
$sid = (int)($row['student_id'] ?? 0);
|
||||||
->whereIn('student_id', $studentIds)
|
|
||||||
->where('semester', $semester)
|
|
||||||
->where('school_year', $schoolYear)
|
|
||||||
->findAll();
|
|
||||||
|
|
||||||
foreach ($dRows as $d) {
|
if ($sid <= 0) {
|
||||||
$decisionMap[(int)$d['student_id']] = $d;
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isset($studentMap[$sid])) {
|
||||||
|
$studentMap[$sid] = [
|
||||||
|
'student_id' => $sid,
|
||||||
|
'school_id' => $row['school_id'] ?? '',
|
||||||
|
'firstname' => $row['firstname'] ?? '',
|
||||||
|
'lastname' => $row['lastname'] ?? '',
|
||||||
|
'class_section_name' => $row['class_section_name'] ?? '',
|
||||||
|
'fall_score' => null,
|
||||||
|
'spring_score' => null,
|
||||||
|
'year_score' => null,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$semKey = strtolower(trim((string)($row['sem_key'] ?? '')));
|
||||||
|
$score = is_numeric($row['semester_score']) ? (float)$row['semester_score'] : null;
|
||||||
|
|
||||||
|
if ($score === null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($semKey === 'fall') {
|
||||||
|
$studentMap[$sid]['fall_score'] = $score;
|
||||||
|
} elseif ($semKey === 'spring') {
|
||||||
|
$studentMap[$sid]['spring_score'] = $score;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach ($rows as &$row) {
|
$rows = [];
|
||||||
$sid = (int)($row['student_id'] ?? 0);
|
|
||||||
|
|
||||||
|
foreach ($studentMap as $sid => $student) {
|
||||||
|
$fall = $student['fall_score'];
|
||||||
|
$spring = $student['spring_score'];
|
||||||
|
|
||||||
|
// Whole-year result requires both semesters.
|
||||||
|
if ($fall === null || $spring === null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$yearScore = round(($fall + $spring) / 2, 2);
|
||||||
|
|
||||||
|
if ($yearScore >= 60) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$student['year_score'] = $yearScore;
|
||||||
|
$rows[$sid] = $student;
|
||||||
|
}
|
||||||
|
|
||||||
|
$studentIds = array_keys($rows);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Load saved below-60 manual decisions.
|
||||||
|
* These are year-level decisions now.
|
||||||
|
*/
|
||||||
|
$decisionMap = [];
|
||||||
|
|
||||||
|
if (!empty($studentIds)) {
|
||||||
|
$belowDecModel = new BelowSixtyDecisionModel();
|
||||||
|
|
||||||
|
$decisionRows = $belowDecModel
|
||||||
|
->whereIn('student_id', $studentIds)
|
||||||
|
->where('semester', 'year')
|
||||||
|
->where('school_year', $schoolYear)
|
||||||
|
->findAll();
|
||||||
|
|
||||||
|
foreach ($decisionRows as $d) {
|
||||||
|
$sid = (int)($d['student_id'] ?? 0);
|
||||||
|
|
||||||
|
if ($sid > 0) {
|
||||||
|
$decisionMap[$sid] = $d;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($rows as $sid => &$row) {
|
||||||
$row['decision'] = $decisionMap[$sid]['decision'] ?? '';
|
$row['decision'] = $decisionMap[$sid]['decision'] ?? '';
|
||||||
$row['decision_notes'] = $decisionMap[$sid]['notes'] ?? '';
|
$row['decision_notes'] = $decisionMap[$sid]['notes'] ?? '';
|
||||||
}
|
}
|
||||||
|
|
||||||
unset($row);
|
unset($row);
|
||||||
|
|
||||||
// ── Load consolidated YEAR decisions from student_decisions ─────────────
|
/*
|
||||||
//
|
* Load final generated whole-year decisions from student_decisions.
|
||||||
// IMPORTANT:
|
* This table is now year-based, so do NOT filter by semester.
|
||||||
// student_decisions no longer has semester or semester_score.
|
*/
|
||||||
// It is now one row per student per school_year using year_score.
|
$finalDecisionMap = [];
|
||||||
$sdMap = [];
|
|
||||||
|
|
||||||
if (!empty($studentIds)) {
|
if (!empty($studentIds)) {
|
||||||
$sdRows = $this->db->table('student_decisions')
|
$finalRows = $db->table('student_decisions')
|
||||||
->whereIn('student_id', $studentIds)
|
->whereIn('student_id', $studentIds)
|
||||||
->where('school_year', $schoolYear)
|
->where('school_year', $schoolYear)
|
||||||
->get()
|
->get()
|
||||||
->getResultArray();
|
->getResultArray();
|
||||||
|
|
||||||
foreach ($sdRows as $sd) {
|
foreach ($finalRows as $fr) {
|
||||||
$sid = (int)($sd['student_id'] ?? 0);
|
$sid = (int)($fr['student_id'] ?? 0);
|
||||||
|
|
||||||
if ($sid > 0) {
|
if ($sid > 0) {
|
||||||
$sdMap[$sid] = $sd;
|
$finalDecisionMap[$sid] = $fr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Load the most recent certificate per student for this school year ───
|
foreach ($rows as $sid => &$row) {
|
||||||
|
$row['consolidated_decision'] = $finalDecisionMap[$sid]['decision'] ?? null;
|
||||||
|
|
||||||
|
if (
|
||||||
|
isset($finalDecisionMap[$sid]['year_score'])
|
||||||
|
&& $finalDecisionMap[$sid]['year_score'] !== ''
|
||||||
|
&& is_numeric($finalDecisionMap[$sid]['year_score'])
|
||||||
|
) {
|
||||||
|
$row['year_score'] = round((float)$finalDecisionMap[$sid]['year_score'], 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
unset($row);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Load certificate numbers.
|
||||||
|
*/
|
||||||
$certMap = [];
|
$certMap = [];
|
||||||
|
|
||||||
if (!empty($studentIds)) {
|
if (!empty($studentIds)) {
|
||||||
$certRows = $this->db->table('certificate_records')
|
$certRows = $db->table('certificate_records')
|
||||||
->select('student_id, certificate_number, issued_at')
|
->select('student_id, certificate_number, issued_at')
|
||||||
->where('school_year', $schoolYear)
|
->where('school_year', $schoolYear)
|
||||||
->whereIn('student_id', $studentIds)
|
->whereIn('student_id', $studentIds)
|
||||||
@@ -2395,16 +2523,15 @@ class GradingController extends Controller
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach ($rows as &$row) {
|
foreach ($rows as $sid => &$row) {
|
||||||
$sid = (int)($row['student_id'] ?? 0);
|
$row['certificate_number'] = $certMap[$sid] ?? '';
|
||||||
|
|
||||||
$row['consolidated_decision'] = $sdMap[$sid]['decision'] ?? null;
|
|
||||||
$row['year_score'] = $sdMap[$sid]['year_score'] ?? null;
|
|
||||||
$row['certificate_number'] = $certMap[$sid] ?? '';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
unset($row);
|
unset($row);
|
||||||
|
|
||||||
|
// Re-index for the view.
|
||||||
|
$rows = array_values($rows);
|
||||||
|
|
||||||
$canViewGrading = $this->userHasMenuUrl('grading');
|
$canViewGrading = $this->userHasMenuUrl('grading');
|
||||||
|
|
||||||
return view('grading/below_sixty_decisions', [
|
return view('grading/below_sixty_decisions', [
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -14,6 +14,47 @@ $totalConfirmed = array_sum(array_column($classResults, 'confirmed'));
|
|||||||
$totalSurprise = array_sum(array_column($classResults, 'surprises'));
|
$totalSurprise = array_sum(array_column($classResults, 'surprises'));
|
||||||
$totalMissed = array_sum(array_column($classResults, 'missed'));
|
$totalMissed = array_sum(array_column($classResults, 'missed'));
|
||||||
$overallAccuracy = $totalPredicted > 0 ? round($totalConfirmed / $totalPredicted * 100) : ($totalActual === 0 ? 100 : 0);
|
$overallAccuracy = $totalPredicted > 0 ? round($totalConfirmed / $totalPredicted * 100) : ($totalActual === 0 ? 100 : 0);
|
||||||
|
|
||||||
|
// Winner gender breakdown.
|
||||||
|
// "Winner" means actual year-end trophy winner.
|
||||||
|
$totalWinnerBoys = 0;
|
||||||
|
$totalWinnerGirls = 0;
|
||||||
|
$totalWinnerOther = 0;
|
||||||
|
|
||||||
|
// Sticker names.
|
||||||
|
// 2 columns x 10 rows = 20 stickers per page.
|
||||||
|
// Stickers print NAME ONLY.
|
||||||
|
$winnerStickerNames = [];
|
||||||
|
|
||||||
|
foreach ($classResults as $cls) {
|
||||||
|
foreach (($cls['students'] ?? []) as $s) {
|
||||||
|
if (empty($s['actual'])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$gender = strtolower(trim((string)($s['gender'] ?? '')));
|
||||||
|
|
||||||
|
if (in_array($gender, ['male', 'm', 'boy', 'boys'], true)) {
|
||||||
|
$totalWinnerBoys++;
|
||||||
|
} elseif (in_array($gender, ['female', 'f', 'girl', 'girls'], true)) {
|
||||||
|
$totalWinnerGirls++;
|
||||||
|
} else {
|
||||||
|
$totalWinnerOther++;
|
||||||
|
}
|
||||||
|
|
||||||
|
$name = trim((string)($s['name'] ?? ''));
|
||||||
|
|
||||||
|
if ($name !== '') {
|
||||||
|
$winnerStickerNames[] = $name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$totalWinners = $totalWinnerBoys + $totalWinnerGirls + $totalWinnerOther;
|
||||||
|
|
||||||
|
$winnerBoysPct = $totalWinners > 0 ? round(($totalWinnerBoys / $totalWinners) * 100, 1) : 0;
|
||||||
|
$winnerGirlsPct = $totalWinners > 0 ? round(($totalWinnerGirls / $totalWinners) * 100, 1) : 0;
|
||||||
|
$winnerOtherPct = $totalWinners > 0 ? round(($totalWinnerOther / $totalWinners) * 100, 1) : 0;
|
||||||
?>
|
?>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@@ -23,23 +64,141 @@ $overallAccuracy = $totalPredicted > 0 ? round($totalConfirmed / $totalPredicted
|
|||||||
.status-none { color:#adb5bd; }
|
.status-none { color:#adb5bd; }
|
||||||
|
|
||||||
.print-only { display: none !important; }
|
.print-only { display: none !important; }
|
||||||
|
.winner-sticker-print-area { display: none; }
|
||||||
|
|
||||||
|
@page {
|
||||||
|
size: Letter portrait;
|
||||||
|
margin: 0.35in;
|
||||||
|
}
|
||||||
|
|
||||||
@media print {
|
@media print {
|
||||||
.no-print { display: none !important; }
|
.no-print { display: none !important; }
|
||||||
.print-only { display: block !important; }
|
.print-only { display: block !important; }
|
||||||
.screen-only { display: none !important; }
|
.screen-only { display: none !important; }
|
||||||
|
|
||||||
body { font-size: 11px; }
|
body { font-size: 11px; }
|
||||||
.container-fluid { padding: 0 !important; }
|
.container-fluid { padding: 0 !important; }
|
||||||
h2, h3 { font-size: 13px; margin-bottom: .3rem; }
|
h2, h3 { font-size: 13px; margin-bottom: .3rem; }
|
||||||
.print-page-break { break-before: page; }
|
.print-page-break { break-before: page; }
|
||||||
|
|
||||||
table { width: 100%; border-collapse: collapse; font-size: 10px; }
|
table { width: 100%; border-collapse: collapse; font-size: 10px; }
|
||||||
table th, table td { border: 1px solid #bbb; padding: 3px 5px; }
|
table th, table td { border: 1px solid #bbb; padding: 3px 5px; }
|
||||||
table thead { background: #333 !important; color: #fff !important;
|
table thead {
|
||||||
-webkit-print-color-adjust: exact; print-color-adjust: exact; }
|
background: #333 !important;
|
||||||
table thead th { position: static !important; top: auto !important; box-shadow: none !important; }
|
color: #fff !important;
|
||||||
|
-webkit-print-color-adjust: exact;
|
||||||
|
print-color-adjust: exact;
|
||||||
|
}
|
||||||
|
table thead th {
|
||||||
|
position: static !important;
|
||||||
|
top: auto !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
.s-confirmed { color: #198754; font-weight: bold; }
|
.s-confirmed { color: #198754; font-weight: bold; }
|
||||||
.s-surprise { color: #0d6efd; font-weight: bold; }
|
.s-surprise { color: #0d6efd; font-weight: bold; }
|
||||||
.s-missed { color: #fd7e14; font-weight: bold; }
|
.s-missed { color: #fd7e14; font-weight: bold; }
|
||||||
|
|
||||||
|
body.print-stickers-mode {
|
||||||
|
margin: 0 !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
background: #fff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.print-stickers-mode * {
|
||||||
|
box-shadow: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.print-stickers-mode header,
|
||||||
|
body.print-stickers-mode nav,
|
||||||
|
body.print-stickers-mode aside,
|
||||||
|
body.print-stickers-mode footer,
|
||||||
|
body.print-stickers-mode .navbar,
|
||||||
|
body.print-stickers-mode .sidebar,
|
||||||
|
body.print-stickers-mode .topbar,
|
||||||
|
body.print-stickers-mode .app-header,
|
||||||
|
body.print-stickers-mode .main-header,
|
||||||
|
body.print-stickers-mode .layout-header,
|
||||||
|
body.print-stickers-mode .management-header,
|
||||||
|
body.print-stickers-mode .page-header,
|
||||||
|
body.print-stickers-mode .breadcrumb,
|
||||||
|
body.print-stickers-mode .brand,
|
||||||
|
body.print-stickers-mode .logo,
|
||||||
|
body.print-stickers-mode .header,
|
||||||
|
body.print-stickers-mode .no-print,
|
||||||
|
body.print-stickers-mode .screen-only,
|
||||||
|
body.print-stickers-mode .print-only {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.print-stickers-mode .container-fluid > *:not(#winnerStickerPrintArea) {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.print-stickers-mode .container-fluid {
|
||||||
|
display: block !important;
|
||||||
|
visibility: visible !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
width: 100% !important;
|
||||||
|
max-width: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.print-stickers-mode #winnerStickerPrintArea {
|
||||||
|
display: block !important;
|
||||||
|
visibility: visible !important;
|
||||||
|
position: static !important;
|
||||||
|
width: 100% !important;
|
||||||
|
height: auto !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
overflow: visible !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.print-stickers-mode #winnerStickerPrintArea,
|
||||||
|
body.print-stickers-mode #winnerStickerPrintArea * {
|
||||||
|
visibility: visible !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.print-stickers-mode .sticker-page {
|
||||||
|
display: grid !important;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
grid-template-rows: repeat(10, 1fr);
|
||||||
|
gap: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 10.3in;
|
||||||
|
page-break-after: always;
|
||||||
|
break-after: page;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.print-stickers-mode .sticker-page:last-child {
|
||||||
|
page-break-after: auto;
|
||||||
|
break-after: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.print-stickers-mode .sticker-cell {
|
||||||
|
display: flex !important;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
min-height: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.print-stickers-mode .sticker-name {
|
||||||
|
display: block !important;
|
||||||
|
font-size: 20pt;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.1;
|
||||||
|
color: #000 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.print-stickers-mode .sticker-empty {
|
||||||
|
visibility: hidden !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
@@ -57,10 +216,16 @@ $overallAccuracy = $totalPredicted > 0 ? round($totalConfirmed / $totalPredicted
|
|||||||
with the <strong>year-end result</strong> based on the average of Fall & Spring scores.
|
with the <strong>year-end result</strong> based on the average of Fall & Spring scores.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex gap-2">
|
|
||||||
|
<div class="d-flex gap-2 flex-wrap">
|
||||||
<button onclick="printWithCharts()" class="btn btn-outline-secondary btn-sm">
|
<button onclick="printWithCharts()" class="btn btn-outline-secondary btn-sm">
|
||||||
<i class="bi bi-printer-fill me-1"></i>Print
|
<i class="bi bi-printer-fill me-1"></i>Print
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<button onclick="printWinnerStickers()" class="btn btn-warning btn-sm">
|
||||||
|
<i class="bi bi-tags-fill me-1"></i>Print Winner Stickers
|
||||||
|
</button>
|
||||||
|
|
||||||
<a href="<?= site_url('administrator/trophy?' . http_build_query(['school_year' => $selectedYear, 'percentile' => $selectedPercentile])) ?>"
|
<a href="<?= site_url('administrator/trophy?' . http_build_query(['school_year' => $selectedYear, 'percentile' => $selectedPercentile])) ?>"
|
||||||
class="btn btn-outline-secondary btn-sm">
|
class="btn btn-outline-secondary btn-sm">
|
||||||
<i class="bi bi-arrow-left me-1"></i>Back
|
<i class="bi bi-arrow-left me-1"></i>Back
|
||||||
@@ -75,18 +240,27 @@ $overallAccuracy = $totalPredicted > 0 ? round($totalConfirmed / $totalPredicted
|
|||||||
<label class="form-label mb-1 small fw-semibold">School Year</label>
|
<label class="form-label mb-1 small fw-semibold">School Year</label>
|
||||||
<select name="school_year" class="form-select form-select-sm" style="min-width:130px;">
|
<select name="school_year" class="form-select form-select-sm" style="min-width:130px;">
|
||||||
<?php foreach ($years as $yr): ?>
|
<?php foreach ($years as $yr): ?>
|
||||||
<option value="<?= esc($yr) ?>" <?= $yr === $selectedYear ? 'selected' : '' ?>><?= esc($yr) ?></option>
|
<option value="<?= esc($yr) ?>" <?= $yr === $selectedYear ? 'selected' : '' ?>>
|
||||||
|
<?= esc($yr) ?>
|
||||||
|
</option>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-auto">
|
<div class="col-auto">
|
||||||
<label class="form-label mb-1 small fw-semibold">Percentile</label>
|
<label class="form-label mb-1 small fw-semibold">Percentile</label>
|
||||||
<div class="input-group input-group-sm" style="width:110px;">
|
<div class="input-group input-group-sm" style="width:110px;">
|
||||||
<input type="number" name="percentile" class="form-control"
|
<input type="number"
|
||||||
min="1" max="99" step="1" value="<?= (int)$selectedPercentile ?>">
|
name="percentile"
|
||||||
|
class="form-control"
|
||||||
|
min="1"
|
||||||
|
max="99"
|
||||||
|
step="1"
|
||||||
|
value="<?= (int)$selectedPercentile ?>">
|
||||||
<span class="input-group-text">%</span>
|
<span class="input-group-text">%</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-auto">
|
<div class="col-auto">
|
||||||
<button type="submit" class="btn btn-primary btn-sm">Apply</button>
|
<button type="submit" class="btn btn-primary btn-sm">Apply</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -111,7 +285,7 @@ $overallAccuracy = $totalPredicted > 0 ? round($totalConfirmed / $totalPredicted
|
|||||||
<div class="row g-3 mb-4">
|
<div class="row g-3 mb-4">
|
||||||
<?php foreach ([
|
<?php foreach ([
|
||||||
[$totalPredicted, 'Predicted (Fall)', 'primary', null, $totalStudents > 0 ? round($totalPredicted / $totalStudents * 100) . '% of students' : '—'],
|
[$totalPredicted, 'Predicted (Fall)', 'primary', null, $totalStudents > 0 ? round($totalPredicted / $totalStudents * 100) . '% of students' : '—'],
|
||||||
[$totalActual, 'Actual (Year)', 'warning', 'dark', $totalStudents > 0 ? round($totalActual / $totalStudents * 100) . '% of students' : '—'],
|
[$totalActual, 'Actual (Year)', 'warning', 'dark', $totalStudents > 0 ? round($totalActual / $totalStudents * 100) . '% of students' : '—'],
|
||||||
[$totalConfirmed, 'Confirmed', 'success', null, $totalPredicted > 0 ? round($totalConfirmed / $totalPredicted * 100) . '% of predicted' : '—'],
|
[$totalConfirmed, 'Confirmed', 'success', null, $totalPredicted > 0 ? round($totalConfirmed / $totalPredicted * 100) . '% of predicted' : '—'],
|
||||||
[$totalSurprise, 'Surprises', 'info', 'dark', 'Not in prediction'],
|
[$totalSurprise, 'Surprises', 'info', 'dark', 'Not in prediction'],
|
||||||
[$totalMissed, 'Missed', 'orange', null, 'Were predicted'],
|
[$totalMissed, 'Missed', 'orange', null, 'Were predicted'],
|
||||||
@@ -147,19 +321,29 @@ $overallAccuracy = $totalPredicted > 0 ? round($totalConfirmed / $totalPredicted
|
|||||||
<span class="fw-bold fs-6"><?= esc($cls['section_name']) ?></span>
|
<span class="fw-bold fs-6"><?= esc($cls['section_name']) ?></span>
|
||||||
<span class="badge bg-primary"><?= $cls['predicted_count'] ?> predicted</span>
|
<span class="badge bg-primary"><?= $cls['predicted_count'] ?> predicted</span>
|
||||||
<span class="badge bg-warning text-dark"><?= $cls['actual_count'] ?> actual</span>
|
<span class="badge bg-warning text-dark"><?= $cls['actual_count'] ?> actual</span>
|
||||||
|
|
||||||
<?php if ($cls['confirmed'] > 0): ?>
|
<?php if ($cls['confirmed'] > 0): ?>
|
||||||
<span class="badge bg-success"><?= $cls['confirmed'] ?> confirmed</span>
|
<span class="badge bg-success"><?= $cls['confirmed'] ?> confirmed</span>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
<?php if ($cls['surprises'] > 0): ?>
|
<?php if ($cls['surprises'] > 0): ?>
|
||||||
<span class="badge bg-info text-dark"><?= $cls['surprises'] ?> surprise<?= $cls['surprises'] > 1 ? 's' : '' ?></span>
|
<span class="badge bg-info text-dark"><?= $cls['surprises'] ?> surprise<?= $cls['surprises'] > 1 ? 's' : '' ?></span>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
<?php if ($cls['missed'] > 0): ?>
|
<?php if ($cls['missed'] > 0): ?>
|
||||||
<span class="badge" style="background:#fd7e14"><?= $cls['missed'] ?> missed</span>
|
<span class="badge" style="background:#fd7e14"><?= $cls['missed'] ?> missed</span>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="d-flex gap-3 small align-items-center">
|
<div class="d-flex gap-3 small align-items-center">
|
||||||
<span class="text-muted">Fall ≥ <strong><?= $cls['fall_threshold'] !== null ? number_format((float)$cls['fall_threshold'], 1) : '—' ?></strong></span>
|
<span class="text-muted">
|
||||||
<span class="text-muted">Year ≥ <strong><?= $cls['year_threshold'] !== null ? number_format((float)$cls['year_threshold'], 1) : '—' ?></strong></span>
|
Fall ≥
|
||||||
|
<strong><?= $cls['fall_threshold'] !== null ? number_format((float)$cls['fall_threshold'], 1) : '—' ?></strong>
|
||||||
|
</span>
|
||||||
|
<span class="text-muted">
|
||||||
|
Year ≥
|
||||||
|
<strong><?= $cls['year_threshold'] !== null ? number_format((float)$cls['year_threshold'], 1) : '—' ?></strong>
|
||||||
|
</span>
|
||||||
<span class="fw-semibold">
|
<span class="fw-semibold">
|
||||||
Accuracy:
|
Accuracy:
|
||||||
<span class="<?= $cls['accuracy'] >= 80 ? 'text-success' : ($cls['accuracy'] >= 50 ? 'text-warning' : 'text-danger') ?>">
|
<span class="<?= $cls['accuracy'] >= 80 ? 'text-success' : ($cls['accuracy'] >= 50 ? 'text-warning' : 'text-danger') ?>">
|
||||||
@@ -168,6 +352,7 @@ $overallAccuracy = $totalPredicted > 0 ? round($totalConfirmed / $totalPredicted
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card-body p-0">
|
<div class="card-body p-0">
|
||||||
<table class="table table-sm table-hover mb-0 align-middle" data-no-mgmt-sticky>
|
<table class="table table-sm table-hover mb-0 align-middle" data-no-mgmt-sticky>
|
||||||
<thead class="table-light">
|
<thead class="table-light">
|
||||||
@@ -183,12 +368,16 @@ $overallAccuracy = $totalPredicted > 0 ? round($totalConfirmed / $totalPredicted
|
|||||||
<th class="text-center">Status</th>
|
<th class="text-center">Status</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
|
||||||
<tbody>
|
<tbody>
|
||||||
<?php $rank = 0; foreach ($cls['students'] as $s):
|
<?php $rank = 0; foreach ($cls['students'] as $s):
|
||||||
if ($s['status'] === 'none') continue;
|
if ($s['status'] === 'none') continue;
|
||||||
$rank++;
|
$rank++;
|
||||||
$isMale = strtolower($s['gender'] ?? '') === 'male';
|
|
||||||
$rowBg = match ($s['status']) {
|
$genderNorm = strtolower(trim((string)($s['gender'] ?? '')));
|
||||||
|
$isMale = in_array($genderNorm, ['male', 'm', 'boy'], true);
|
||||||
|
|
||||||
|
$rowBg = match ($s['status']) {
|
||||||
'confirmed' => 'table-success',
|
'confirmed' => 'table-success',
|
||||||
'surprise' => 'table-primary',
|
'surprise' => 'table-primary',
|
||||||
'missed' => 'table-warning',
|
'missed' => 'table-warning',
|
||||||
@@ -203,9 +392,9 @@ $overallAccuracy = $totalPredicted > 0 ? round($totalConfirmed / $totalPredicted
|
|||||||
<?= $isMale ? 'M' : 'F' ?>
|
<?= $isMale ? 'M' : 'F' ?>
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="text-end small"><?= $s['fall_score'] !== null ? number_format($s['fall_score'], 1) : '<span class="text-muted">—</span>' ?></td>
|
<td class="text-end small"><?= $s['fall_score'] !== null ? number_format($s['fall_score'], 1) : '<span class="text-muted">—</span>' ?></td>
|
||||||
<td class="text-end small"><?= $s['spring_score'] !== null ? number_format($s['spring_score'], 1) : '<span class="text-muted">—</span>' ?></td>
|
<td class="text-end small"><?= $s['spring_score'] !== null ? number_format($s['spring_score'], 1) : '<span class="text-muted">—</span>' ?></td>
|
||||||
<td class="text-end small fw-semibold"><?= $s['year_score'] !== null ? number_format($s['year_score'], 1) : '<span class="text-muted">—</span>' ?></td>
|
<td class="text-end small fw-semibold"><?= $s['year_score'] !== null ? number_format($s['year_score'], 1) : '<span class="text-muted">—</span>' ?></td>
|
||||||
<td class="text-center small">
|
<td class="text-center small">
|
||||||
<?= $s['predicted']
|
<?= $s['predicted']
|
||||||
? '<span class="badge bg-primary">Yes</span>'
|
? '<span class="badge bg-primary">Yes</span>'
|
||||||
@@ -237,6 +426,7 @@ $overallAccuracy = $totalPredicted > 0 ? round($totalConfirmed / $totalPredicted
|
|||||||
<div class="card-header bg-dark text-white fw-semibold py-2">
|
<div class="card-header bg-dark text-white fw-semibold py-2">
|
||||||
<i class="bi bi-bar-chart-fill me-2"></i>Prediction Accuracy Summary — <?= esc($selectedYear) ?>
|
<i class="bi bi-bar-chart-fill me-2"></i>Prediction Accuracy Summary — <?= esc($selectedYear) ?>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card-body p-0">
|
<div class="card-body p-0">
|
||||||
<table class="table table-sm table-bordered mb-0 align-middle" data-no-mgmt-sticky>
|
<table class="table table-sm table-bordered mb-0 align-middle" data-no-mgmt-sticky>
|
||||||
<thead class="table-dark">
|
<thead class="table-dark">
|
||||||
@@ -253,6 +443,7 @@ $overallAccuracy = $totalPredicted > 0 ? round($totalConfirmed / $totalPredicted
|
|||||||
<th class="text-end pe-2">Year ≥</th>
|
<th class="text-end pe-2">Year ≥</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
|
||||||
<tbody>
|
<tbody>
|
||||||
<?php foreach ($classResults as $cls): ?>
|
<?php foreach ($classResults as $cls): ?>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -271,6 +462,7 @@ $overallAccuracy = $totalPredicted > 0 ? round($totalConfirmed / $totalPredicted
|
|||||||
</tr>
|
</tr>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|
||||||
<tfoot class="table-secondary fw-semibold">
|
<tfoot class="table-secondary fw-semibold">
|
||||||
<tr>
|
<tr>
|
||||||
<td class="ps-2">Total</td>
|
<td class="ps-2">Total</td>
|
||||||
@@ -292,7 +484,6 @@ $overallAccuracy = $totalPredicted > 0 ? round($totalConfirmed / $totalPredicted
|
|||||||
|
|
||||||
<!-- Charts -->
|
<!-- Charts -->
|
||||||
<div class="row g-4 mt-1 mb-4">
|
<div class="row g-4 mt-1 mb-4">
|
||||||
<!-- Grouped bar: predicted / actual / confirmed / surprises / missed per class -->
|
|
||||||
<div class="col-12 col-lg-8">
|
<div class="col-12 col-lg-8">
|
||||||
<div class="border rounded p-3 h-100">
|
<div class="border rounded p-3 h-100">
|
||||||
<div class="small fw-semibold text-muted mb-2">
|
<div class="small fw-semibold text-muted mb-2">
|
||||||
@@ -301,7 +492,7 @@ $overallAccuracy = $totalPredicted > 0 ? round($totalConfirmed / $totalPredicted
|
|||||||
<canvas id="chart-counts" style="max-height:280px;"></canvas>
|
<canvas id="chart-counts" style="max-height:280px;"></canvas>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Bar: accuracy % per class + doughnut overall breakdown -->
|
|
||||||
<div class="col-12 col-lg-4">
|
<div class="col-12 col-lg-4">
|
||||||
<div class="row g-3 h-100">
|
<div class="row g-3 h-100">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
@@ -312,6 +503,7 @@ $overallAccuracy = $totalPredicted > 0 ? round($totalConfirmed / $totalPredicted
|
|||||||
<canvas id="chart-accuracy" style="max-height:130px;"></canvas>
|
<canvas id="chart-accuracy" style="max-height:130px;"></canvas>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<div class="border rounded p-3">
|
<div class="border rounded p-3">
|
||||||
<div class="small fw-semibold text-muted mb-2">
|
<div class="small fw-semibold text-muted mb-2">
|
||||||
@@ -324,6 +516,93 @@ $overallAccuracy = $totalPredicted > 0 ? round($totalConfirmed / $totalPredicted
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Winner gender summary -->
|
||||||
|
<div class="card shadow-sm mt-2 mb-4">
|
||||||
|
<div class="card-header bg-dark text-white fw-semibold py-2">
|
||||||
|
<i class="bi bi-gender-ambiguous me-2"></i>Winner Gender Breakdown
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row g-3 align-items-stretch">
|
||||||
|
<div class="col-12 col-lg-3">
|
||||||
|
<div class="border rounded p-3 text-center h-100">
|
||||||
|
<div class="text-muted small mb-1">Total Winners</div>
|
||||||
|
<div class="display-6 fw-bold"><?= (int)$totalWinners ?></div>
|
||||||
|
<div class="small text-muted">Actual year-end trophy winners</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-6 col-lg-3">
|
||||||
|
<div class="border rounded p-3 text-center h-100">
|
||||||
|
<div class="text-muted small mb-1">Boys</div>
|
||||||
|
<div class="display-6 fw-bold text-primary"><?= (int)$totalWinnerBoys ?></div>
|
||||||
|
<div class="fw-semibold"><?= number_format($winnerBoysPct, 1) ?>%</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-6 col-lg-3">
|
||||||
|
<div class="border rounded p-3 text-center h-100">
|
||||||
|
<div class="text-muted small mb-1">Girls</div>
|
||||||
|
<div class="display-6 fw-bold" style="color:#E47AB0;"><?= (int)$totalWinnerGirls ?></div>
|
||||||
|
<div class="fw-semibold"><?= number_format($winnerGirlsPct, 1) ?>%</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12 col-lg-3">
|
||||||
|
<div class="border rounded p-3 text-center h-100">
|
||||||
|
<div class="text-muted small mb-1">Unknown / Other</div>
|
||||||
|
<div class="display-6 fw-bold text-secondary"><?= (int)$totalWinnerOther ?></div>
|
||||||
|
<div class="fw-semibold"><?= number_format($winnerOtherPct, 1) ?>%</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-3">
|
||||||
|
<div class="progress" style="height:26px;">
|
||||||
|
<?php if ($totalWinners > 0): ?>
|
||||||
|
<div class="progress-bar bg-primary"
|
||||||
|
role="progressbar"
|
||||||
|
style="width: <?= $winnerBoysPct ?>%;"
|
||||||
|
aria-valuenow="<?= $winnerBoysPct ?>"
|
||||||
|
aria-valuemin="0"
|
||||||
|
aria-valuemax="100">
|
||||||
|
Boys <?= number_format($winnerBoysPct, 1) ?>%
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="progress-bar"
|
||||||
|
role="progressbar"
|
||||||
|
style="width: <?= $winnerGirlsPct ?>%; background:#E47AB0;"
|
||||||
|
aria-valuenow="<?= $winnerGirlsPct ?>"
|
||||||
|
aria-valuemin="0"
|
||||||
|
aria-valuemax="100">
|
||||||
|
Girls <?= number_format($winnerGirlsPct, 1) ?>%
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if ($totalWinnerOther > 0): ?>
|
||||||
|
<div class="progress-bar bg-secondary"
|
||||||
|
role="progressbar"
|
||||||
|
style="width: <?= $winnerOtherPct ?>%;"
|
||||||
|
aria-valuenow="<?= $winnerOtherPct ?>"
|
||||||
|
aria-valuemin="0"
|
||||||
|
aria-valuemax="100">
|
||||||
|
Other <?= number_format($winnerOtherPct, 1) ?>%
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php else: ?>
|
||||||
|
<div class="progress-bar bg-secondary"
|
||||||
|
role="progressbar"
|
||||||
|
style="width: 100%;"
|
||||||
|
aria-valuenow="0"
|
||||||
|
aria-valuemin="0"
|
||||||
|
aria-valuemax="100">
|
||||||
|
No winners
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div><!-- /screen-only -->
|
</div><!-- /screen-only -->
|
||||||
|
|
||||||
<!-- ══ PRINT VIEW ════════════════════════════════════════════════════════ -->
|
<!-- ══ PRINT VIEW ════════════════════════════════════════════════════════ -->
|
||||||
@@ -340,7 +619,6 @@ $overallAccuracy = $totalPredicted > 0 ? round($totalConfirmed / $totalPredicted
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- All students flat table -->
|
|
||||||
<h3 style="margin-bottom:4px;">Student Detail</h3>
|
<h3 style="margin-bottom:4px;">Student Detail</h3>
|
||||||
<table data-no-mgmt-sticky>
|
<table data-no-mgmt-sticky>
|
||||||
<thead>
|
<thead>
|
||||||
@@ -357,6 +635,7 @@ $overallAccuracy = $totalPredicted > 0 ? round($totalConfirmed / $totalPredicted
|
|||||||
<th style="text-align:center;">Status</th>
|
<th style="text-align:center;">Status</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
|
||||||
<tbody>
|
<tbody>
|
||||||
<?php
|
<?php
|
||||||
$rank = 0;
|
$rank = 0;
|
||||||
@@ -364,7 +643,8 @@ $overallAccuracy = $totalPredicted > 0 ? round($totalConfirmed / $totalPredicted
|
|||||||
foreach ($cls['students'] as $s):
|
foreach ($cls['students'] as $s):
|
||||||
if (!in_array($s['status'], ['confirmed', 'surprise'], true)) continue;
|
if (!in_array($s['status'], ['confirmed', 'surprise'], true)) continue;
|
||||||
$rank++;
|
$rank++;
|
||||||
$isMale = strtolower($s['gender'] ?? '') === 'male';
|
$genderNorm = strtolower(trim((string)($s['gender'] ?? '')));
|
||||||
|
$isMale = in_array($genderNorm, ['male', 'm', 'boy'], true);
|
||||||
$statusLabel = match ($s['status']) {
|
$statusLabel = match ($s['status']) {
|
||||||
'confirmed' => '✓ Confirmed',
|
'confirmed' => '✓ Confirmed',
|
||||||
'surprise' => '↑ Surprise',
|
'surprise' => '↑ Surprise',
|
||||||
@@ -378,18 +658,17 @@ $overallAccuracy = $totalPredicted > 0 ? round($totalConfirmed / $totalPredicted
|
|||||||
<td><?= esc($cls['section_name']) ?></td>
|
<td><?= esc($cls['section_name']) ?></td>
|
||||||
<td><strong><?= esc($s['name']) ?></strong></td>
|
<td><strong><?= esc($s['name']) ?></strong></td>
|
||||||
<td style="text-align:center;"><?= $isMale ? 'M' : 'F' ?></td>
|
<td style="text-align:center;"><?= $isMale ? 'M' : 'F' ?></td>
|
||||||
<td style="text-align:right;"><?= $s['fall_score'] !== null ? number_format($s['fall_score'], 1) : '—' ?></td>
|
<td style="text-align:right;"><?= $s['fall_score'] !== null ? number_format($s['fall_score'], 1) : '—' ?></td>
|
||||||
<td style="text-align:right;"><?= $s['spring_score'] !== null ? number_format($s['spring_score'], 1) : '—' ?></td>
|
<td style="text-align:right;"><?= $s['spring_score'] !== null ? number_format($s['spring_score'], 1) : '—' ?></td>
|
||||||
<td style="text-align:right;font-weight:bold;"><?= $s['year_score'] !== null ? number_format($s['year_score'], 1) : '—' ?></td>
|
<td style="text-align:right;font-weight:bold;"><?= $s['year_score'] !== null ? number_format($s['year_score'], 1) : '—' ?></td>
|
||||||
<td style="text-align:center;"><?= $s['predicted'] ? 'Yes' : 'No' ?></td>
|
<td style="text-align:center;"><?= $s['predicted'] ? 'Yes' : 'No' ?></td>
|
||||||
<td style="text-align:center;"><?= $s['actual'] ? 'Yes' : 'No' ?></td>
|
<td style="text-align:center;"><?= $s['actual'] ? 'Yes' : 'No' ?></td>
|
||||||
<td style="text-align:center;" class="<?= $statusClass ?>"><?= $statusLabel ?></td>
|
<td style="text-align:center;" class="<?= $statusClass ?>"><?= $statusLabel ?></td>
|
||||||
</tr>
|
</tr>
|
||||||
<?php endforeach; endforeach; ?>
|
<?php endforeach; endforeach; ?>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<!-- Accuracy summary (new page) -->
|
|
||||||
<div class="print-page-break"></div>
|
<div class="print-page-break"></div>
|
||||||
<h3 style="margin-bottom:4px;">Prediction Accuracy Summary</h3>
|
<h3 style="margin-bottom:4px;">Prediction Accuracy Summary</h3>
|
||||||
<table data-no-mgmt-sticky style="margin-bottom:14px;">
|
<table data-no-mgmt-sticky style="margin-bottom:14px;">
|
||||||
@@ -407,6 +686,7 @@ $overallAccuracy = $totalPredicted > 0 ? round($totalConfirmed / $totalPredicted
|
|||||||
<th style="text-align:right;">Year ≥</th>
|
<th style="text-align:right;">Year ≥</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
|
||||||
<tbody>
|
<tbody>
|
||||||
<?php foreach ($classResults as $cls): ?>
|
<?php foreach ($classResults as $cls): ?>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -423,6 +703,7 @@ $overallAccuracy = $totalPredicted > 0 ? round($totalConfirmed / $totalPredicted
|
|||||||
</tr>
|
</tr>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|
||||||
<tfoot>
|
<tfoot>
|
||||||
<tr>
|
<tr>
|
||||||
<td style="font-weight:bold;">Total</td>
|
<td style="font-weight:bold;">Total</td>
|
||||||
@@ -438,17 +719,18 @@ $overallAccuracy = $totalPredicted > 0 ? round($totalConfirmed / $totalPredicted
|
|||||||
</tfoot>
|
</tfoot>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<!-- Charts as images (populated by JS before printing) -->
|
|
||||||
<div class="print-page-break"></div>
|
<div class="print-page-break"></div>
|
||||||
<h3 style="margin-bottom:6px;">Charts</h3>
|
<h3 style="margin-bottom:6px;">Charts</h3>
|
||||||
|
|
||||||
<div style="margin-bottom:14px;">
|
<div style="margin-bottom:14px;">
|
||||||
<p style="font-size:10px;font-weight:bold;margin:0 0 4px;">Trophy Counts per Class</p>
|
<p style="font-size:10px;font-weight:bold;margin:0 0 4px;">Trophy Counts per Class</p>
|
||||||
<img id="print-chart-counts" style="width:100%;max-height:220px;object-fit:contain;" src="" alt="">
|
<img id="print-chart-counts" style="width:100%;max-height:220px;object-fit:contain;" src="" alt="">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="display:flex;gap:16px;margin-bottom:14px;">
|
<div style="display:flex;gap:16px;margin-bottom:14px;">
|
||||||
<div style="flex:1;">
|
<div style="flex:1;">
|
||||||
<p style="font-size:10px;font-weight:bold;margin:0 0 4px;">Prediction Accuracy per Class</p>
|
<p style="font-size:10px;font-weight:bold;margin:0 0 4px;">Prediction Accuracy per Class</p>
|
||||||
<img id="print-chart-accuracy" style="width:100%;max-height:160px;object-fit:contain;" src="" alt="">
|
<img id="print-chart-accuracy" style="width:100%;max-height:160px;object-fit:contain;" src="" alt="">
|
||||||
</div>
|
</div>
|
||||||
<div style="flex:1;">
|
<div style="flex:1;">
|
||||||
<p style="font-size:10px;font-weight:bold;margin:0 0 4px;">Overall Outcome Breakdown</p>
|
<p style="font-size:10px;font-weight:bold;margin:0 0 4px;">Overall Outcome Breakdown</p>
|
||||||
@@ -456,8 +738,69 @@ $overallAccuracy = $totalPredicted > 0 ? round($totalConfirmed / $totalPredicted
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-top:10px;border:1px solid #bbb;padding:8px;">
|
||||||
|
<p style="font-size:11px;font-weight:bold;margin:0 0 6px;">Winner Gender Breakdown</p>
|
||||||
|
|
||||||
|
<table data-no-mgmt-sticky>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Group</th>
|
||||||
|
<th style="text-align:center;">Winners</th>
|
||||||
|
<th style="text-align:center;">Percentage</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>Boys</td>
|
||||||
|
<td style="text-align:center;"><?= (int)$totalWinnerBoys ?></td>
|
||||||
|
<td style="text-align:center;"><?= number_format($winnerBoysPct, 1) ?>%</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Girls</td>
|
||||||
|
<td style="text-align:center;"><?= (int)$totalWinnerGirls ?></td>
|
||||||
|
<td style="text-align:center;"><?= number_format($winnerGirlsPct, 1) ?>%</td>
|
||||||
|
</tr>
|
||||||
|
<?php if ($totalWinnerOther > 0): ?>
|
||||||
|
<tr>
|
||||||
|
<td>Unknown / Other</td>
|
||||||
|
<td style="text-align:center;"><?= (int)$totalWinnerOther ?></td>
|
||||||
|
<td style="text-align:center;"><?= number_format($winnerOtherPct, 1) ?>%</td>
|
||||||
|
</tr>
|
||||||
|
<?php endif; ?>
|
||||||
|
</tbody>
|
||||||
|
|
||||||
|
<tfoot>
|
||||||
|
<tr>
|
||||||
|
<td style="font-weight:bold;">Total Winners</td>
|
||||||
|
<td style="text-align:center;font-weight:bold;"><?= (int)$totalWinners ?></td>
|
||||||
|
<td style="text-align:center;font-weight:bold;"><?= $totalWinners > 0 ? '100.0%' : '0.0%' ?></td>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div><!-- /print-only -->
|
</div><!-- /print-only -->
|
||||||
|
|
||||||
|
<!-- Winner sticker print area: names only, no header, no score, no class -->
|
||||||
|
<div id="winnerStickerPrintArea" class="winner-sticker-print-area">
|
||||||
|
<?php if (!empty($winnerStickerNames)): ?>
|
||||||
|
<?php foreach (array_chunk($winnerStickerNames, 20) as $chunk): ?>
|
||||||
|
<div class="sticker-page">
|
||||||
|
<?php foreach ($chunk as $winnerName): ?>
|
||||||
|
<div class="sticker-cell">
|
||||||
|
<div class="sticker-name"><?= esc($winnerName) ?></div>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
|
||||||
|
<?php for ($i = 0, $remaining = 20 - count($chunk); $i < $remaining; $i++): ?>
|
||||||
|
<div class="sticker-cell sticker-empty"></div>
|
||||||
|
<?php endfor; ?>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -473,6 +816,7 @@ $overallAccuracy = $totalPredicted > 0 ? round($totalConfirmed / $totalPredicted
|
|||||||
$cSurprise = [];
|
$cSurprise = [];
|
||||||
$cMissed = [];
|
$cMissed = [];
|
||||||
$cAccuracy = [];
|
$cAccuracy = [];
|
||||||
|
|
||||||
foreach ($classResults as $cls) {
|
foreach ($classResults as $cls) {
|
||||||
$cLabels[] = $cls['section_name'];
|
$cLabels[] = $cls['section_name'];
|
||||||
$cPredicted[] = $cls['predicted_count'];
|
$cPredicted[] = $cls['predicted_count'];
|
||||||
@@ -483,94 +827,105 @@ $overallAccuracy = $totalPredicted > 0 ? round($totalConfirmed / $totalPredicted
|
|||||||
$cAccuracy[] = $cls['accuracy'];
|
$cAccuracy[] = $cls['accuracy'];
|
||||||
}
|
}
|
||||||
?>
|
?>
|
||||||
var labels = <?= json_encode($cLabels) ?>;
|
|
||||||
|
var labels = <?= json_encode($cLabels) ?>;
|
||||||
var predicted = <?= json_encode($cPredicted) ?>;
|
var predicted = <?= json_encode($cPredicted) ?>;
|
||||||
var actual = <?= json_encode($cActual) ?>;
|
var actual = <?= json_encode($cActual) ?>;
|
||||||
var confirmed = <?= json_encode($cConfirmed) ?>;
|
var confirmed = <?= json_encode($cConfirmed) ?>;
|
||||||
var surprises = <?= json_encode($cSurprise) ?>;
|
var surprises = <?= json_encode($cSurprise) ?>;
|
||||||
var missed = <?= json_encode($cMissed) ?>;
|
var missed = <?= json_encode($cMissed) ?>;
|
||||||
var accuracy = <?= json_encode($cAccuracy) ?>;
|
var accuracy = <?= json_encode($cAccuracy) ?>;
|
||||||
|
|
||||||
/* ── Chart 1: grouped bar – counts per class ── */
|
if (document.getElementById('chart-counts')) {
|
||||||
new Chart(document.getElementById('chart-counts'), {
|
new Chart(document.getElementById('chart-counts'), {
|
||||||
type: 'bar',
|
type: 'bar',
|
||||||
data: {
|
data: {
|
||||||
labels: labels,
|
labels: labels,
|
||||||
datasets: [
|
datasets: [
|
||||||
{ label: 'Predicted', data: predicted, backgroundColor: '#4A90E2', borderRadius: 3 },
|
{ label: 'Predicted', data: predicted, backgroundColor: '#4A90E2', borderRadius: 3 },
|
||||||
{ label: 'Actual', data: actual, backgroundColor: '#f0a500', borderRadius: 3 },
|
{ label: 'Actual', data: actual, backgroundColor: '#f0a500', borderRadius: 3 },
|
||||||
{ label: 'Confirmed', data: confirmed, backgroundColor: '#28a745', borderRadius: 3 },
|
{ label: 'Confirmed', data: confirmed, backgroundColor: '#28a745', borderRadius: 3 },
|
||||||
{ label: 'Surprises', data: surprises, backgroundColor: '#17a2b8', borderRadius: 3 },
|
{ label: 'Surprises', data: surprises, backgroundColor: '#17a2b8', borderRadius: 3 },
|
||||||
{ label: 'Missed', data: missed, backgroundColor: '#fd7e14', borderRadius: 3 },
|
{ label: 'Missed', data: missed, backgroundColor: '#fd7e14', borderRadius: 3 },
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
options: {
|
options: {
|
||||||
responsive: true,
|
responsive: true,
|
||||||
maintainAspectRatio: true,
|
maintainAspectRatio: true,
|
||||||
plugins: { legend: { position: 'bottom' } },
|
plugins: { legend: { position: 'bottom' } },
|
||||||
scales: {
|
scales: {
|
||||||
x: { grid: { color: '#f0f0f0' } },
|
x: { grid: { color: '#f0f0f0' } },
|
||||||
y: { beginAtZero: true, ticks: { stepSize: 1 }, grid: { color: '#f0f0f0' } }
|
y: { beginAtZero: true, ticks: { stepSize: 1 }, grid: { color: '#f0f0f0' } }
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/* ── Chart 2: accuracy % per class ── */
|
|
||||||
new Chart(document.getElementById('chart-accuracy'), {
|
|
||||||
type: 'bar',
|
|
||||||
data: {
|
|
||||||
labels: labels,
|
|
||||||
datasets: [{
|
|
||||||
label: 'Accuracy %',
|
|
||||||
data: accuracy,
|
|
||||||
backgroundColor: accuracy.map(function(a) {
|
|
||||||
return a >= 80 ? '#28a745' : a >= 50 ? '#f0a500' : '#dc3545';
|
|
||||||
}),
|
|
||||||
borderRadius: 3
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
options: {
|
|
||||||
responsive: true,
|
|
||||||
maintainAspectRatio: true,
|
|
||||||
plugins: { legend: { display: false } },
|
|
||||||
scales: {
|
|
||||||
y: {
|
|
||||||
beginAtZero: true, max: 100,
|
|
||||||
ticks: { callback: function(v) { return v + '%'; } },
|
|
||||||
grid: { color: '#f0f0f0' }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
});
|
}
|
||||||
|
|
||||||
/* ── Chart 3: doughnut overall outcome breakdown ── */
|
if (document.getElementById('chart-accuracy')) {
|
||||||
new Chart(document.getElementById('chart-breakdown'), {
|
new Chart(document.getElementById('chart-accuracy'), {
|
||||||
type: 'doughnut',
|
type: 'bar',
|
||||||
data: {
|
data: {
|
||||||
labels: ['Confirmed', 'Surprises', 'Missed'],
|
labels: labels,
|
||||||
datasets: [{
|
datasets: [{
|
||||||
data: [<?= $totalConfirmed ?>, <?= $totalSurprise ?>, <?= $totalMissed ?>],
|
label: 'Accuracy %',
|
||||||
backgroundColor: ['#28a745', '#17a2b8', '#fd7e14'],
|
data: accuracy,
|
||||||
borderWidth: 2
|
backgroundColor: accuracy.map(function(a) {
|
||||||
}]
|
return a >= 80 ? '#28a745' : a >= 50 ? '#f0a500' : '#dc3545';
|
||||||
},
|
}),
|
||||||
options: {
|
borderRadius: 3
|
||||||
responsive: true,
|
}]
|
||||||
maintainAspectRatio: true,
|
},
|
||||||
plugins: {
|
options: {
|
||||||
legend: { position: 'bottom' },
|
responsive: true,
|
||||||
tooltip: {
|
maintainAspectRatio: true,
|
||||||
callbacks: {
|
plugins: { legend: { display: false } },
|
||||||
label: function(ctx) {
|
scales: {
|
||||||
var total = ctx.dataset.data.reduce(function(a, b) { return a + b; }, 0);
|
y: {
|
||||||
var pct = total > 0 ? Math.round(ctx.parsed / total * 100) : 0;
|
beginAtZero: true,
|
||||||
return ctx.label + ': ' + ctx.parsed + ' (' + pct + '%)';
|
max: 100,
|
||||||
|
ticks: {
|
||||||
|
callback: function(v) {
|
||||||
|
return v + '%';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
grid: { color: '#f0f0f0' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.getElementById('chart-breakdown')) {
|
||||||
|
new Chart(document.getElementById('chart-breakdown'), {
|
||||||
|
type: 'doughnut',
|
||||||
|
data: {
|
||||||
|
labels: ['Confirmed', 'Surprises', 'Missed'],
|
||||||
|
datasets: [{
|
||||||
|
data: [<?= $totalConfirmed ?>, <?= $totalSurprise ?>, <?= $totalMissed ?>],
|
||||||
|
backgroundColor: ['#28a745', '#17a2b8', '#fd7e14'],
|
||||||
|
borderWidth: 2
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: true,
|
||||||
|
plugins: {
|
||||||
|
legend: { position: 'bottom' },
|
||||||
|
tooltip: {
|
||||||
|
callbacks: {
|
||||||
|
label: function(ctx) {
|
||||||
|
var total = ctx.dataset.data.reduce(function(a, b) {
|
||||||
|
return a + b;
|
||||||
|
}, 0);
|
||||||
|
var pct = total > 0 ? Math.round(ctx.parsed / total * 100) : 0;
|
||||||
|
return ctx.label + ': ' + ctx.parsed + ' (' + pct + '%)';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
});
|
}
|
||||||
})();
|
})();
|
||||||
|
|
||||||
function captureCharts() {
|
function captureCharts() {
|
||||||
@@ -579,20 +934,41 @@ function captureCharts() {
|
|||||||
'chart-accuracy': 'print-chart-accuracy',
|
'chart-accuracy': 'print-chart-accuracy',
|
||||||
'chart-breakdown': 'print-chart-breakdown',
|
'chart-breakdown': 'print-chart-breakdown',
|
||||||
};
|
};
|
||||||
|
|
||||||
Object.keys(map).forEach(function(canvasId) {
|
Object.keys(map).forEach(function(canvasId) {
|
||||||
var canvas = document.getElementById(canvasId);
|
var canvas = document.getElementById(canvasId);
|
||||||
var img = document.getElementById(map[canvasId]);
|
var img = document.getElementById(map[canvasId]);
|
||||||
if (canvas && img) img.src = canvas.toDataURL('image/png');
|
|
||||||
|
if (canvas && img) {
|
||||||
|
img.src = canvas.toDataURL('image/png');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function printWithCharts() {
|
function printWithCharts() {
|
||||||
|
document.body.classList.remove('print-stickers-mode');
|
||||||
captureCharts();
|
captureCharts();
|
||||||
window.print();
|
window.print();
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener('beforeprint', captureCharts);
|
function printWinnerStickers() {
|
||||||
|
document.body.classList.add('print-stickers-mode');
|
||||||
|
|
||||||
|
setTimeout(function () {
|
||||||
|
window.print();
|
||||||
|
|
||||||
|
setTimeout(function () {
|
||||||
|
document.body.classList.remove('print-stickers-mode');
|
||||||
|
}, 1000);
|
||||||
|
}, 250);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('beforeprint', function () {
|
||||||
|
if (!document.body.classList.contains('print-stickers-mode')) {
|
||||||
|
captureCharts();
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
<?= $this->endSection() ?>
|
<?= $this->endSection() ?>
|
||||||
|
|
||||||
<?= $this->endSection() ?>
|
<?= $this->endSection() ?>
|
||||||
@@ -1,28 +1,71 @@
|
|||||||
<?= $this->extend('layout/management_layout') ?>
|
<?= $this->extend('layout/management_layout') ?>
|
||||||
<?= $this->section('content') ?>
|
<?= $this->section('content') ?>
|
||||||
|
|
||||||
|
<?php
|
||||||
|
// This page is Fall semester only.
|
||||||
|
// Do not expose Whole Year mode here.
|
||||||
|
$semester = 'fall';
|
||||||
|
$actionSemester = 'fall';
|
||||||
|
|
||||||
|
$schoolYear = $schoolYear ?? '';
|
||||||
|
$schoolYears = $schoolYears ?? [];
|
||||||
|
|
||||||
|
if (empty($schoolYears) && $schoolYear !== '') {
|
||||||
|
$schoolYears = [$schoolYear];
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<div class="wrapper below-sixty-wrapper">
|
<div class="wrapper below-sixty-wrapper">
|
||||||
<h2 class="text-center mt-4 mb-4 below-sixty-title">Below 60 Summary</h2>
|
<h2 class="text-center mt-4 mb-4 below-sixty-title">Below 60 Summary</h2>
|
||||||
|
|
||||||
<?= $this->include('partials/academic_filter') ?>
|
<!-- School year filter only -->
|
||||||
|
<div class="card shadow-sm mb-3">
|
||||||
|
<div class="card-body py-3">
|
||||||
|
<form method="get"
|
||||||
|
action="<?= site_url('grading/below-60') ?>"
|
||||||
|
class="row g-2 align-items-end justify-content-center">
|
||||||
|
|
||||||
|
<input type="hidden" name="semester" value="fall">
|
||||||
|
|
||||||
|
<div class="col-12 col-sm-auto">
|
||||||
|
<label class="form-label mb-1 small fw-semibold">School Year</label>
|
||||||
|
|
||||||
|
<?php if (!empty($schoolYears)): ?>
|
||||||
|
<select name="school_year"
|
||||||
|
class="form-select form-select-sm"
|
||||||
|
style="min-width:160px;">
|
||||||
|
<?php foreach ($schoolYears as $yr): ?>
|
||||||
|
<option value="<?= esc($yr) ?>" <?= (string)$yr === (string)$schoolYear ? 'selected' : '' ?>>
|
||||||
|
<?= esc($yr) ?>
|
||||||
|
</option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
<?php else: ?>
|
||||||
|
<input type="text"
|
||||||
|
name="school_year"
|
||||||
|
class="form-control form-control-sm"
|
||||||
|
style="min-width:160px;"
|
||||||
|
value="<?= esc($schoolYear) ?>"
|
||||||
|
placeholder="2025-2026">
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12 col-sm-auto">
|
||||||
|
<button type="submit" class="btn btn-sm btn-primary">
|
||||||
|
<i class="bi bi-funnel me-1"></i>Apply
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="d-flex justify-content-between align-items-center mb-3 flex-wrap gap-2">
|
<div class="d-flex justify-content-between align-items-center mb-3 flex-wrap gap-2">
|
||||||
<div class="text-muted">
|
<div class="text-muted">
|
||||||
<?= !empty($isYearMode) ? 'Whole Year' : 'Fall' ?> • <?= esc($schoolYear ?? '') ?>
|
Fall • <?= esc($schoolYear) ?>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="d-flex gap-2">
|
<div class="d-flex gap-2">
|
||||||
<?php if (!empty($isYearMode)): ?>
|
|
||||||
<a class="btn btn-outline-primary btn-sm"
|
|
||||||
href="<?= site_url('grading/below-60/decisions?' . http_build_query([
|
|
||||||
'semester' => 'year',
|
|
||||||
'school_year' => $schoolYear ?? '',
|
|
||||||
])) ?>">
|
|
||||||
Decisions
|
|
||||||
</a>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<?php if (!empty($canViewGrading)): ?>
|
<?php if (!empty($canViewGrading)): ?>
|
||||||
<a class="btn btn-outline-secondary btn-sm" href="<?= base_url('grading') ?>">
|
<a class="btn btn-outline-secondary btn-sm" href="<?= base_url('grading') ?>">
|
||||||
Back to Grading
|
Back to Grading
|
||||||
@@ -43,15 +86,11 @@
|
|||||||
|
|
||||||
return esc($value);
|
return esc($value);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Fall mode uses Fall.
|
|
||||||
// Whole Year mode uses year.
|
|
||||||
$actionSemester = !empty($isYearMode) ? 'year' : 'fall';
|
|
||||||
?>
|
?>
|
||||||
|
|
||||||
<?php if (empty($rows)): ?>
|
<?php if (empty($rows)): ?>
|
||||||
<div class="alert alert-success text-center d-inline-block">
|
<div class="alert alert-success text-center d-inline-block">
|
||||||
No students below 60 for this selection.
|
No students below 60 for Fall in <?= esc($schoolYear) ?>.
|
||||||
</div>
|
</div>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<div class="table-responsive below-sixty-table">
|
<div class="table-responsive below-sixty-table">
|
||||||
@@ -62,7 +101,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<th>Student Name</th>
|
<th>Student Name</th>
|
||||||
<th>Section</th>
|
<th>Section</th>
|
||||||
<th class="text-center">Score</th>
|
<th class="text-center">Fall Score</th>
|
||||||
<th>Status</th>
|
<th>Status</th>
|
||||||
<th>Email Parent</th>
|
<th>Email Parent</th>
|
||||||
<th>Schedule Meeting</th>
|
<th>Schedule Meeting</th>
|
||||||
@@ -72,6 +111,7 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
<?php foreach ($rows as $row): ?>
|
<?php foreach ($rows as $row): ?>
|
||||||
<?php
|
<?php
|
||||||
|
// Fall-only page: use semester_score.
|
||||||
$scoreRaw = $row['semester_score'] ?? null;
|
$scoreRaw = $row['semester_score'] ?? null;
|
||||||
$scoreVal = is_numeric($scoreRaw) ? (float)$scoreRaw : null;
|
$scoreVal = is_numeric($scoreRaw) ? (float)$scoreRaw : null;
|
||||||
|
|
||||||
@@ -102,7 +142,7 @@
|
|||||||
style="font-size:0.72rem;padding:1px 7px;"
|
style="font-size:0.72rem;padding:1px 7px;"
|
||||||
data-student-id="<?= (int)($row['student_id'] ?? 0) ?>"
|
data-student-id="<?= (int)($row['student_id'] ?? 0) ?>"
|
||||||
data-student-name="<?= esc($studentLabel) ?>"
|
data-student-name="<?= esc($studentLabel) ?>"
|
||||||
data-school-year="<?= esc((string)($schoolYear ?? '')) ?>">
|
data-school-year="<?= esc((string)$schoolYear) ?>">
|
||||||
Details
|
Details
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
@@ -119,15 +159,15 @@
|
|||||||
|
|
||||||
<input type="hidden"
|
<input type="hidden"
|
||||||
name="semester"
|
name="semester"
|
||||||
value="<?= esc($actionSemester) ?>">
|
value="fall">
|
||||||
|
|
||||||
<input type="hidden"
|
<input type="hidden"
|
||||||
name="school_year"
|
name="school_year"
|
||||||
value="<?= esc((string)($schoolYear ?? '')) ?>">
|
value="<?= esc((string)$schoolYear) ?>">
|
||||||
|
|
||||||
<select name="status"
|
<select name="status"
|
||||||
class="form-select form-select-sm"
|
class="form-select form-select-sm"
|
||||||
style="width: 110px;">
|
style="width:110px;">
|
||||||
<option value="Open" <?= ($row['status'] ?? 'Open') === 'Open' ? 'selected' : '' ?>>
|
<option value="Open" <?= ($row['status'] ?? 'Open') === 'Open' ? 'selected' : '' ?>>
|
||||||
Open
|
Open
|
||||||
</option>
|
</option>
|
||||||
@@ -139,7 +179,7 @@
|
|||||||
<input type="text"
|
<input type="text"
|
||||||
name="note"
|
name="note"
|
||||||
class="form-control form-control-sm"
|
class="form-control form-control-sm"
|
||||||
style="width: 140px;"
|
style="width:140px;"
|
||||||
placeholder="Note (optional)"
|
placeholder="Note (optional)"
|
||||||
value="<?= esc((string)($row['note'] ?? '')) ?>">
|
value="<?= esc((string)($row['note'] ?? '')) ?>">
|
||||||
|
|
||||||
@@ -156,7 +196,11 @@
|
|||||||
</button>
|
</button>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<a class="btn btn-sm btn-outline-primary"
|
<a class="btn btn-sm btn-outline-primary"
|
||||||
href="<?= site_url('grading/below-60/email/edit?student_id=' . (int)($row['student_id'] ?? 0) . '&semester=' . rawurlencode($actionSemester) . '&school_year=' . rawurlencode((string)($schoolYear ?? ''))) ?>">
|
href="<?= site_url('grading/below-60/email/edit?' . http_build_query([
|
||||||
|
'student_id' => (int)($row['student_id'] ?? 0),
|
||||||
|
'semester' => 'fall',
|
||||||
|
'school_year' => (string)$schoolYear,
|
||||||
|
])) ?>">
|
||||||
Send Email
|
Send Email
|
||||||
</a>
|
</a>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
@@ -169,7 +213,11 @@
|
|||||||
</button>
|
</button>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<a class="btn btn-sm btn-outline-secondary"
|
<a class="btn btn-sm btn-outline-secondary"
|
||||||
href="<?= site_url('grading/below-60/schedule?student_id=' . (int)($row['student_id'] ?? 0) . '&semester=' . rawurlencode($actionSemester) . '&school_year=' . rawurlencode((string)($schoolYear ?? ''))) ?>">
|
href="<?= site_url('grading/below-60/schedule?' . http_build_query([
|
||||||
|
'student_id' => (int)($row['student_id'] ?? 0),
|
||||||
|
'semester' => 'fall',
|
||||||
|
'school_year' => (string)$schoolYear,
|
||||||
|
])) ?>">
|
||||||
Schedule
|
Schedule
|
||||||
</a>
|
</a>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
@@ -246,21 +294,6 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
(function () {
|
(function () {
|
||||||
function normalizeSemesterFilter() {
|
|
||||||
const semesterSelect = document.querySelector('select[name="semester"]');
|
|
||||||
|
|
||||||
if (!semesterSelect) return;
|
|
||||||
|
|
||||||
const wholeYearSelected = <?= !empty($isYearMode) ? 'true' : 'false' ?>;
|
|
||||||
|
|
||||||
semesterSelect.innerHTML = '';
|
|
||||||
semesterSelect.add(new Option('Fall', 'fall', !wholeYearSelected, !wholeYearSelected));
|
|
||||||
semesterSelect.add(new Option('Whole Year', 'year', wholeYearSelected, wholeYearSelected));
|
|
||||||
semesterSelect.value = wholeYearSelected ? 'year' : 'fall';
|
|
||||||
}
|
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', normalizeSemesterFilter);
|
|
||||||
|
|
||||||
if (window.$ && $.fn && $.fn.DataTable) {
|
if (window.$ && $.fn && $.fn.DataTable) {
|
||||||
$(function () {
|
$(function () {
|
||||||
const table = $('.below-sixty-dt');
|
const table = $('.below-sixty-dt');
|
||||||
|
|||||||
@@ -5,29 +5,77 @@
|
|||||||
// This page is Whole Year only.
|
// This page is Whole Year only.
|
||||||
// Do not expose semester filter here.
|
// Do not expose semester filter here.
|
||||||
$semester = 'year';
|
$semester = 'year';
|
||||||
|
|
||||||
|
$schoolYear = $schoolYear ?? '';
|
||||||
|
$schoolYears = $schoolYears ?? [];
|
||||||
|
|
||||||
|
if (empty($schoolYears) && $schoolYear !== '') {
|
||||||
|
$schoolYears = [$schoolYear];
|
||||||
|
}
|
||||||
?>
|
?>
|
||||||
|
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<div class="wrapper below-sixty-decisions-wrapper">
|
<div class="wrapper below-sixty-decisions-wrapper">
|
||||||
<h2 class="text-center mt-4 mb-4">Below 60 — Whole Year Decisions</h2>
|
<h2 class="text-center mt-4 mb-4">School Year Decisions</h2>
|
||||||
|
|
||||||
|
<!-- School year filter -->
|
||||||
|
<div class="card shadow-sm mb-3">
|
||||||
|
<div class="card-body py-3">
|
||||||
|
<form method="get"
|
||||||
|
action="<?= site_url('grading/below-60/decisions') ?>"
|
||||||
|
class="row g-2 align-items-end justify-content-center">
|
||||||
|
|
||||||
|
<input type="hidden" name="semester" value="year">
|
||||||
|
|
||||||
|
<div class="col-12 col-sm-auto">
|
||||||
|
<label class="form-label mb-1 small fw-semibold">School Year</label>
|
||||||
|
|
||||||
|
<?php if (!empty($schoolYears)): ?>
|
||||||
|
<select name="school_year"
|
||||||
|
class="form-select form-select-sm"
|
||||||
|
style="min-width: 160px;">
|
||||||
|
<?php foreach ($schoolYears as $yr): ?>
|
||||||
|
<option value="<?= esc($yr) ?>" <?= (string)$yr === (string)$schoolYear ? 'selected' : '' ?>>
|
||||||
|
<?= esc($yr) ?>
|
||||||
|
</option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
<?php else: ?>
|
||||||
|
<input type="text"
|
||||||
|
name="school_year"
|
||||||
|
class="form-control form-control-sm"
|
||||||
|
style="min-width: 160px;"
|
||||||
|
value="<?= esc($schoolYear) ?>"
|
||||||
|
placeholder="2025-2026">
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12 col-sm-auto">
|
||||||
|
<button type="submit" class="btn btn-sm btn-primary">
|
||||||
|
<i class="bi bi-funnel me-1"></i>Apply
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="d-flex justify-content-between align-items-center mb-3 flex-wrap gap-2">
|
<div class="d-flex justify-content-between align-items-center mb-3 flex-wrap gap-2">
|
||||||
<div class="text-muted">
|
<div class="text-muted">
|
||||||
Whole Year • <?= esc($schoolYear ?? '') ?>
|
Whole Year • <?= esc($schoolYear) ?>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="d-flex gap-2 flex-wrap">
|
<div class="d-flex gap-2 flex-wrap">
|
||||||
<a class="btn btn-outline-secondary btn-sm"
|
<a class="btn btn-outline-secondary btn-sm"
|
||||||
href="<?= site_url('grading/below-60?' . http_build_query([
|
href="<?= site_url('grading/below-60?' . http_build_query([
|
||||||
'semester' => 'year',
|
'semester' => 'year',
|
||||||
'school_year' => $schoolYear ?? '',
|
'school_year' => $schoolYear,
|
||||||
])) ?>">
|
])) ?>">
|
||||||
← Back to Below 60
|
← Back to Below 60
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a class="btn btn-outline-primary btn-sm"
|
<a class="btn btn-outline-primary btn-sm"
|
||||||
href="<?= site_url('grading/decisions?' . http_build_query([
|
href="<?= site_url('grading/decisions?' . http_build_query([
|
||||||
'school_year' => $schoolYear ?? '',
|
'school_year' => $schoolYear,
|
||||||
])) ?>">
|
])) ?>">
|
||||||
All Decisions
|
All Decisions
|
||||||
</a>
|
</a>
|
||||||
@@ -89,7 +137,7 @@ $semester = 'year';
|
|||||||
|
|
||||||
<?php if (empty($rows)): ?>
|
<?php if (empty($rows)): ?>
|
||||||
<div class="alert alert-success text-center d-inline-block">
|
<div class="alert alert-success text-center d-inline-block">
|
||||||
No students below 60 for the whole year.
|
No students below 60 for the whole year in <?= esc($schoolYear) ?>.
|
||||||
</div>
|
</div>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
@@ -99,21 +147,19 @@ $semester = 'year';
|
|||||||
<thead class="table-light">
|
<thead class="table-light">
|
||||||
<tr>
|
<tr>
|
||||||
<th style="min-width:160px">Student Name</th>
|
<th style="min-width:160px">Student Name</th>
|
||||||
<th>Section</th>
|
<th>Class-Section</th>
|
||||||
<th class="text-center">Year Score</th>
|
<th class="text-center">Year Score</th>
|
||||||
<th style="min-width:300px">Comments / Rationale & Decision</th>
|
<th style="min-width:300px">Comments / Rationale & Decision</th>
|
||||||
<th style="min-width:150px" class="text-center">Below-60 Decision</th>
|
<th style="min-width:150px" class="text-center">Below-60 Decision</th>
|
||||||
<th style="min-width:130px" class="text-center">Final Decision</th>
|
|
||||||
<th style="min-width:130px" class="text-center">Certificate</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
|
||||||
<tbody>
|
<tbody>
|
||||||
<?php foreach ($rows as $row): ?>
|
<?php foreach ($rows as $row): ?>
|
||||||
<?php
|
<?php
|
||||||
// Whole-year score. Prefer year_score if the controller provides it.
|
// Whole-year page: use year_score only.
|
||||||
// Fall/Spring details remain available through the Details modal.
|
// Do not fall back to semester_score, because this page should not display semester results.
|
||||||
$scoreRaw = $row['year_score'] ?? $row['semester_score'] ?? null;
|
$scoreRaw = $row['year_score'] ?? null;
|
||||||
$scoreVal = is_numeric($scoreRaw) ? (float)$scoreRaw : null;
|
$scoreVal = is_numeric($scoreRaw) ? (float)$scoreRaw : null;
|
||||||
|
|
||||||
$rowClass = '';
|
$rowClass = '';
|
||||||
@@ -126,11 +172,6 @@ $semester = 'year';
|
|||||||
$currentDecision = (string)($row['decision'] ?? '');
|
$currentDecision = (string)($row['decision'] ?? '');
|
||||||
$currentNotes = (string)($row['decision_notes'] ?? '');
|
$currentNotes = (string)($row['decision_notes'] ?? '');
|
||||||
$badge = $decisionBadge[$currentDecision] ?? null;
|
$badge = $decisionBadge[$currentDecision] ?? null;
|
||||||
|
|
||||||
$finalDecision = $row['consolidated_decision'] ?? null;
|
|
||||||
$finalBadge = $finalDecision !== null ? ($decisionBadge[$finalDecision] ?? 'secondary') : null;
|
|
||||||
|
|
||||||
$certNumber = (string)($row['certificate_number'] ?? '');
|
|
||||||
?>
|
?>
|
||||||
|
|
||||||
<tr class="<?= esc($rowClass) ?>">
|
<tr class="<?= esc($rowClass) ?>">
|
||||||
@@ -146,7 +187,7 @@ $semester = 'year';
|
|||||||
style="font-size:0.72rem;padding:1px 7px;"
|
style="font-size:0.72rem;padding:1px 7px;"
|
||||||
data-student-id="<?= (int)($row['student_id'] ?? 0) ?>"
|
data-student-id="<?= (int)($row['student_id'] ?? 0) ?>"
|
||||||
data-student-name="<?= esc($studentLabel) ?>"
|
data-student-name="<?= esc($studentLabel) ?>"
|
||||||
data-school-year="<?= esc((string)($schoolYear ?? '')) ?>">
|
data-school-year="<?= esc((string)$schoolYear) ?>">
|
||||||
Details
|
Details
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
@@ -165,7 +206,7 @@ $semester = 'year';
|
|||||||
|
|
||||||
<input type="hidden"
|
<input type="hidden"
|
||||||
name="school_year"
|
name="school_year"
|
||||||
value="<?= esc((string)($schoolYear ?? '')) ?>">
|
value="<?= esc((string)$schoolYear) ?>">
|
||||||
|
|
||||||
<textarea name="notes"
|
<textarea name="notes"
|
||||||
class="form-control form-control-sm decision-notes"
|
class="form-control form-control-sm decision-notes"
|
||||||
@@ -199,41 +240,13 @@ $semester = 'year';
|
|||||||
class="btn btn-sm btn-outline-primary btn-send-email"
|
class="btn btn-sm btn-outline-primary btn-send-email"
|
||||||
data-student-id="<?= (int)($row['student_id'] ?? 0) ?>"
|
data-student-id="<?= (int)($row['student_id'] ?? 0) ?>"
|
||||||
data-semester="year"
|
data-semester="year"
|
||||||
data-school-year="<?= esc((string)($schoolYear ?? '')) ?>">
|
data-school-year="<?= esc((string)$schoolYear) ?>">
|
||||||
Send Email
|
Send Email
|
||||||
</button>
|
</button>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<span class="text-muted small">Pending</span>
|
<span class="text-muted small">Pending</span>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td class="text-center align-middle">
|
|
||||||
<?php if ($finalDecision !== null && $finalDecision !== ''): ?>
|
|
||||||
<span class="badge bg-<?= esc($finalBadge) ?> px-2 py-1">
|
|
||||||
<?= esc($finalDecision) ?>
|
|
||||||
</span>
|
|
||||||
<?php else: ?>
|
|
||||||
<a href="<?= site_url('grading/decisions?' . http_build_query([
|
|
||||||
'school_year' => $schoolYear ?? '',
|
|
||||||
])) ?>"
|
|
||||||
class="text-muted small">
|
|
||||||
Generate
|
|
||||||
</a>
|
|
||||||
<?php endif; ?>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td class="text-center align-middle">
|
|
||||||
<?php if ($certNumber !== ''): ?>
|
|
||||||
<a href="<?= site_url('administrator/certificates/reprint/' . rawurlencode($certNumber)) ?>"
|
|
||||||
target="_blank"
|
|
||||||
class="font-monospace text-decoration-none fw-semibold"
|
|
||||||
title="Click to reprint certificate">
|
|
||||||
<?= esc($certNumber) ?>
|
|
||||||
</a>
|
|
||||||
<?php else: ?>
|
|
||||||
<span class="text-muted small">—</span>
|
|
||||||
<?php endif; ?>
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|||||||
Reference in New Issue
Block a user