Compare commits

..

3 Commits

Author SHA1 Message Date
root 090cb88573 fix attendance coemment in report card 2026-05-30 17:12:10 -04:00
root 95bcefc3a9 fix decision email 2026-05-30 16:55:20 -04:00
root f24f4311e8 fix delibration and below 60 2026-05-30 15:00:16 -04:00
12 changed files with 2719 additions and 225 deletions
View File
BIN
View File
Binary file not shown.
+10 -3
View File
@@ -528,10 +528,17 @@ $routes->post('grading/decisions/generate', 'View\GradingController::generateAll
$routes->get('grading/below-60/decisions', 'View\GradingController::belowSixtyDecisions', ['filter' => 'auth:read']); $routes->get('grading/below-60/decisions', 'View\GradingController::belowSixtyDecisions', ['filter' => 'auth:read']);
$routes->post('grading/below-60/decisions/save', 'View\GradingController::saveBelowSixtyDecision', ['filter' => 'auth:read']); $routes->post('grading/below-60/decisions/save', 'View\GradingController::saveBelowSixtyDecision', ['filter' => 'auth:read']);
$routes->get('grading/below-60/decisions/student-details', 'View\GradingController::studentDecisionDetails', ['filter' => 'auth:read']); $routes->get('grading/below-60/decisions/student-details', 'View\GradingController::studentDecisionDetails', ['filter' => 'auth:read']);
$routes->get('grading/below-60/decisions/email/preview', 'View\GradingController::previewDecisionEmail', ['filter' => 'auth:read']); $routes->get(
$routes->get('grading/below-60/decisions/email/edit', 'View\GradingController::editDecisionEmail', ['filter' => 'auth:read']); 'grading/below-60/decisions/email/preview',
$routes->post('grading/below-60/decisions/email', 'View\GradingController::sendDecisionEmail', ['filter' => 'auth:read']); 'View\GradingController::previewBelowSixtyDecisionEmail',
['filter' => 'auth:read']
);
$routes->post(
'grading/below-60/decisions/email',
'View\GradingController::sendBelowSixtyDecisionEmail',
['filter' => 'auth:read']
);
// Final part // Final part
$routes->get('grading/(:segment)/(:num)/(:num)', 'View\GradingController::show/$1/$2/$3'); $routes->get('grading/(:segment)/(:num)/(:num)', 'View\GradingController::show/$1/$2/$3');
+771 -84
View File
@@ -1068,22 +1068,56 @@ 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; // This page is Fall only.
$semester = 'fall';
$isYearMode = false;
$schoolYears = $this->getSchoolYearsForScores($schoolYear); $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); $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'); $canViewGrading = $this->userHasMenuUrl('grading');
return view('grading/below_sixty', [ return view('grading/below_sixty', [
@@ -1091,14 +1125,10 @@ class GradingController extends Controller
'semester' => $semester, 'semester' => $semester,
'schoolYear' => $schoolYear, 'schoolYear' => $schoolYear,
'schoolYears' => $schoolYears, 'schoolYears' => $schoolYears,
'canViewGrading' => $canViewGrading,
'isYearMode' => $isYearMode, 'isYearMode' => $isYearMode,
'semesterOptions' => ['Fall'], 'canViewGrading' => $canViewGrading,
'showAllSemesterOption' => true,
]); ]);
} }
public function editBelowSixtyEmail() public function editBelowSixtyEmail()
{ {
$studentId = (int)$this->request->getGet('student_id'); $studentId = (int)$this->request->getGet('student_id');
@@ -2300,85 +2330,183 @@ class GradingController extends Controller
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();
$studentMap = [];
foreach ($scoreRows as $row) {
$sid = (int)($row['student_id'] ?? 0);
if ($sid <= 0) {
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;
}
}
$rows = [];
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 = []; $decisionMap = [];
if (!empty($studentIds)) { if (!empty($studentIds)) {
$dRows = $decisionModel $belowDecModel = new BelowSixtyDecisionModel();
$decisionRows = $belowDecModel
->whereIn('student_id', $studentIds) ->whereIn('student_id', $studentIds)
->where('semester', $semester) ->where('semester', 'year')
->where('school_year', $schoolYear) ->where('school_year', $schoolYear)
->findAll(); ->findAll();
foreach ($dRows as $d) { foreach ($decisionRows as $d) {
$decisionMap[(int)$d['student_id']] = $d; $sid = (int)($d['student_id'] ?? 0);
if ($sid > 0) {
$decisionMap[$sid] = $d;
}
} }
} }
foreach ($rows as &$row) { foreach ($rows as $sid => &$row) {
$sid = (int)($row['student_id'] ?? 0);
$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['consolidated_decision'] = $sdMap[$sid]['decision'] ?? null;
$row['year_score'] = $sdMap[$sid]['year_score'] ?? null;
$row['certificate_number'] = $certMap[$sid] ?? ''; $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', [
@@ -2416,51 +2543,148 @@ class GradingController extends Controller
]); ]);
} }
public function saveBelowSixtyDecision() public function saveBelowSixtyDecision()
{ {
$studentId = (int)$this->request->getPost('student_id'); $studentId = (int)($this->request->getPost('student_id') ?? 0);
$semester = trim((string)$this->request->getPost('semester')); $semester = strtolower(trim((string)($this->request->getPost('semester') ?? 'year')));
$schoolYear = trim((string)$this->request->getPost('school_year')); $schoolYear = trim((string)($this->request->getPost('school_year') ?? ''));
$decision = trim((string)$this->request->getPost('decision')); $decision = trim((string)($this->request->getPost('decision') ?? ''));
$notes = trim((string)$this->request->getPost('notes')); $notes = trim((string)($this->request->getPost('notes') ?? ''));
if ($studentId <= 0 || $semester === '' || $schoolYear === '') { if ($studentId <= 0 || $schoolYear === '') {
return redirect()->back()->with('error', 'Missing required data.'); return redirect()->back()->with('error', 'Missing student or school year.');
} }
$allowed = ['', 'Pass', 'Repeat Class', 'Make-up exam in fall', 'Deferred decision', 'Expel', 'Withdrawn']; // This decision page should feed certificate decisions as whole-year decisions.
if (!in_array($decision, $allowed, true)) { // Force year mode here so certificate logic receives final year decision.
return redirect()->back()->with('error', 'Invalid decision value.'); $semester = 'year';
}
$decisionModel = new BelowSixtyDecisionModel(); $db = \Config\Database::connect();
$existing = $decisionModel
/*
* 1. Save/update the manual decision in below_sixty_decisions.
* This keeps your below-60 page history working.
*/
$belowModel = new \App\Models\BelowSixtyDecisionModel();
$existingBelow = $belowModel
->where('student_id', $studentId) ->where('student_id', $studentId)
->where('semester', $semester) ->where('semester', $semester)
->where('school_year', $schoolYear) ->where('school_year', $schoolYear)
->first(); ->first();
$userId = (int)(session()->get('user_id') ?? 0) ?: null; $belowPayload = [
$payload = [ 'student_id' => $studentId,
'decision' => $decision !== '' ? $decision : null, 'semester' => $semester,
'notes' => $notes !== '' ? $notes : null, 'school_year' => $schoolYear,
'decided_by' => $userId, 'decision' => $decision,
'notes' => $notes,
]; ];
if ($existing) { if ($existingBelow) {
$decisionModel->update((int)$existing['id'], $payload); $belowModel->update((int)$existingBelow['id'], $belowPayload);
} else { } else {
$payload['student_id'] = $studentId; $belowModel->insert($belowPayload);
$payload['semester'] = $semester;
$payload['school_year'] = $schoolYear;
$decisionModel->insert($payload);
} }
$query = http_build_query(['semester' => $semester, 'school_year' => $schoolYear]); /*
return redirect()->to(base_url('grading/below-60/decisions') . ($query ? '?' . $query : '')) * 2. Calculate the student's whole-year score.
->with('status', 'Decision saved.'); * Certificate page uses student_decisions.year_score.
*/
$scoreRows = $db->table('semester_scores ss')
->select([
'LOWER(TRIM(ss.semester)) AS sem_key',
'ss.semester_score',
'cs.class_section_name',
])
->join('classSection cs', 'cs.class_section_id = ss.class_section_id', 'left')
->where('ss.student_id', $studentId)
->where('ss.school_year', $schoolYear)
->whereIn('LOWER(TRIM(ss.semester))', ['fall', 'spring'])
->where('ss.semester_score IS NOT NULL', null, false)
->orderBy('ss.updated_at', 'DESC')
->orderBy('ss.id', 'DESC')
->get()
->getResultArray();
$fallScore = null;
$springScore = null;
$classSectionName = null;
foreach ($scoreRows as $sr) {
$semKey = strtolower(trim((string)($sr['sem_key'] ?? '')));
$score = is_numeric($sr['semester_score'] ?? null) ? (float)$sr['semester_score'] : null;
if ($classSectionName === null && !empty($sr['class_section_name'])) {
$classSectionName = (string)$sr['class_section_name'];
} }
if ($semKey === 'fall' && $fallScore === null) {
$fallScore = $score;
}
if ($semKey === 'spring' && $springScore === null) {
$springScore = $score;
}
}
if ($fallScore !== null && $springScore !== null) {
$yearScore = round(($fallScore + $springScore) / 2, 2);
} elseif ($fallScore !== null) {
$yearScore = round($fallScore, 2);
} elseif ($springScore !== null) {
$yearScore = round($springScore, 2);
} else {
$yearScore = null;
}
/*
* 3. Sync into student_decisions.
* This is the part your certificate page needs.
*
* student_decisions is now year-based:
* - no semester
* - no semester_score
* - uses year_score
*/
$source = $decision === '' ? 'pending' : 'manual';
$studentDecisionPayload = [
'student_id' => $studentId,
'school_year' => $schoolYear,
'class_section_name' => $classSectionName,
'year_score' => $yearScore,
'decision' => $decision !== '' ? $decision : null,
'source' => $source,
'notes' => $notes !== '' ? $notes : null,
'generated_by' => (int)(session()->get('user_id') ?? 0) ?: null,
];
$existingStudentDecision = $db->table('student_decisions')
->where('student_id', $studentId)
->where('school_year', $schoolYear)
->get()
->getRowArray();
if ($existingStudentDecision) {
$db->table('student_decisions')
->where('id', (int)$existingStudentDecision['id'])
->update($studentDecisionPayload);
} else {
$db->table('student_decisions')
->insert($studentDecisionPayload);
}
$query = http_build_query([
'semester' => 'year',
'school_year' => $schoolYear,
]);
return redirect()
->to(base_url('grading/below-60/decisions') . '?' . $query)
->with('status', 'Decision saved and certificate decision updated.');
}
public function studentDecisionDetails() public function studentDecisionDetails()
{ {
$studentId = (int)$this->request->getGet('student_id'); $studentId = (int)$this->request->getGet('student_id');
@@ -2475,6 +2699,469 @@ class GradingController extends Controller
]); ]);
} }
public function previewBelowSixtyDecisionEmail()
{
$studentId = (int)($this->request->getGet('student_id') ?? 0);
$schoolYear = trim((string)($this->request->getGet('school_year') ?? ''));
if ($studentId <= 0 || $schoolYear === '') {
return $this->response->setJSON([
'error' => 'Missing student or school year.',
]);
}
/*
* Whole-year decision email.
* Do NOT query semester_scores.semester = "year".
* The Details button uses fetchAllSemestersForStudent(), so this email does too.
*/
$context = $this->getBelowSixtyDecisionEmailContext($studentId, $schoolYear);
if (empty($context['student'])) {
return $this->response->setJSON([
'error' => 'Student not found.',
]);
}
if (empty($context['decision_row']) || trim((string)($context['decision_row']['decision'] ?? '')) === '') {
return $this->response->setJSON([
'error' => 'No saved decision found for this student. Save a decision first.',
]);
}
$studentName = (string)($context['student_name'] ?? 'Student');
$classSectionName = (string)($context['class_section_name'] ?? '');
$decisionRow = $context['decision_row'];
$decision = trim((string)($decisionRow['decision'] ?? ''));
$notes = trim((string)($decisionRow['notes'] ?? ''));
$fallScore = $context['fall_score'] ?? null;
$springScore = $context['spring_score'] ?? null;
$yearScore = $context['year_score'] ?? null;
/*
* Same data used by the Details modal.
*/
$allSemesters = $context['all_semesters'] ?? [];
if (empty($allSemesters)) {
$allSemesters = $this->fetchAllSemestersForStudent($studentId, $schoolYear);
}
$fallText = $fallScore !== null
? number_format((float)$fallScore, 2)
: 'N/A';
$springText = $springScore !== null
? number_format((float)$springScore, 2)
: 'N/A';
$yearText = $yearScore !== null
? number_format((float)$yearScore, 2)
: 'N/A';
$subject = 'Whole Year Academic Decision — ' . $studentName . ' (' . $schoolYear . ')';
$html = '
<p>Dear Parent/Guardian,</p>
<p>
This message is regarding <strong>' . esc($studentName) . '</strong>
for the <strong>' . esc($schoolYear) . '</strong> school year.
</p>
<p>
Class: <strong>' . esc($classSectionName !== '' ? $classSectionName : 'N/A') . '</strong><br>
Fall Score: <strong>' . esc($fallText) . '</strong><br>
Spring Score: <strong>' . esc($springText) . '</strong><br>
Whole-Year Score: <strong>' . esc($yearText) . '</strong><br>
Decision: <strong>' . esc($decision) . '</strong>
</p>
';
if ($notes !== '') {
$html .= '
<p>
<strong>Decision Notes:</strong><br>
' . nl2br(esc($notes)) . '
</p>
';
}
/*
* Add the exact Details-button score/comment content into the email.
*/
$html .= $this->buildDecisionEmailDetailsHtml($allSemesters);
$html .= '
<p>
Please contact the school administration if you have any questions.
</p>
<p>
Regards,<br>
Al Rahma Sunday School
</p>
';
return $this->response->setJSON([
'ok' => true,
'subject' => $subject,
'html' => $html,
'decision' => $decision,
'score' => $yearScore,
]);
}
private function buildDecisionEmailDetailsHtml(array $semesters): string
{
if (empty($semesters)) {
return '
<p><strong>Detailed Scores and Comments</strong></p>
<p>No detailed score data found.</p>
';
}
$scoreLabels = [
'homework_avg' => 'Homework Avg',
'project_avg' => 'Project Avg',
'participation_score' => 'Participation',
'test_avg' => 'Test Avg',
'ptap_score' => 'PTAP Score',
'attendance_score' => 'Attendance',
'midterm_exam_score' => 'Midterm Score',
'final_exam_score' => 'Final Exam',
'semester_score' => 'Semester Score',
];
$commentTypeLabels = [
'general' => 'General',
'attendance' => 'Attendance',
'attendance_comment' => 'Attendance',
'midterm' => 'Midterm',
'final' => 'Final Exam',
'ptap' => 'PTAP',
];
$html = '
<hr>
<p><strong>Detailed Scores and Comments</strong></p>
';
foreach ($semesters as $sem) {
$semesterName = trim((string)($sem['semester'] ?? ''));
if ($semesterName === '') {
$semesterName = 'Semester';
}
$classSectionName = trim((string)($sem['class_section_name'] ?? ''));
$html .= '
<p style="margin-top:16px;margin-bottom:6px;">
<strong>' . esc($semesterName) . ' Semester</strong>';
if ($classSectionName !== '') {
$html .= ' — ' . esc($classSectionName);
}
$html .= '
</p>
<table border="1"
cellpadding="6"
cellspacing="0"
style="border-collapse:collapse;width:100%;margin-bottom:10px;">
<thead>
<tr>
<th align="left" style="background:#f2f2f2;">Item</th>
<th align="right" style="background:#f2f2f2;">Score</th>
</tr>
</thead>
<tbody>
';
$hasScoreRow = false;
foreach ($scoreLabels as $key => $label) {
if (!array_key_exists($key, $sem)) {
continue;
}
$value = $sem[$key];
if ($value === null || $value === '') {
continue;
}
$scoreText = is_numeric($value)
? number_format((float)$value, 2)
: (string)$value;
$fontWeight = $key === 'semester_score' ? 'font-weight:bold;' : '';
$html .= '
<tr>
<td>' . esc($label) . '</td>
<td align="right" style="' . $fontWeight . '">' . esc($scoreText) . '</td>
</tr>
';
$hasScoreRow = true;
}
if (!$hasScoreRow) {
$html .= '
<tr>
<td colspan="2">No scores recorded.</td>
</tr>
';
}
$html .= '
</tbody>
</table>
';
$comments = $sem['comments'] ?? [];
if (is_array($comments) && !empty($comments)) {
$deduped = [];
$seen = [];
foreach ($comments as $type => $text) {
$text = trim((string)$text);
if ($text === '') {
continue;
}
$label = $commentTypeLabels[$type] ?? (string)$type;
$key = $label . '|' . $text;
if (isset($seen[$key])) {
continue;
}
$seen[$key] = true;
$deduped[] = [
'label' => $label,
'text' => $text,
];
}
if (!empty($deduped)) {
$html .= '
<p style="margin:8px 0 4px;"><strong>Comments</strong></p>
';
foreach ($deduped as $comment) {
$html .= '
<p style="margin:4px 0 8px;padding:8px;background:#f8f9fa;border-left:4px solid #999;">
<strong>' . esc($comment['label']) . ':</strong><br>
' . nl2br(esc($comment['text'])) . '
</p>
';
}
}
}
}
return $html;
}
public function sendBelowSixtyDecisionEmail()
{
$studentId = (int)($this->request->getPost('student_id') ?? 0);
$schoolYear = trim((string)($this->request->getPost('school_year') ?? ''));
$subjectInput = trim((string)($this->request->getPost('subject') ?? ''));
$htmlInput = (string)($this->request->getPost('html') ?? '');
if ($studentId <= 0 || $schoolYear === '') {
return redirect()->back()->with('error', 'Missing student or school year.');
}
/*
* Whole-year decision email.
* Do NOT use sendBelowSixtyEmail(), because that one is semester-score based.
*/
$semester = 'year';
$context = $this->getBelowSixtyDecisionEmailContext($studentId, $schoolYear);
if (empty($context['student'])) {
return redirect()->back()->with('error', 'Student not found.');
}
if (empty($context['decision_row']) || trim((string)($context['decision_row']['decision'] ?? '')) === '') {
return redirect()->back()->with('error', 'No saved decision found for this student.');
}
$studentName = $context['student_name'];
$classSectionName = $context['class_section_name'];
$decisionRow = $context['decision_row'];
$subject = $subjectInput !== ''
? $subjectInput
: 'Whole Year Academic Decision — ' . $studentName . ' (' . $schoolYear . ')';
$payload = [
'student_id' => $studentId,
'student_name' => $studentName,
'class_section_name' => $classSectionName,
'semester' => 'year',
'school_year' => $schoolYear,
'decision' => (string)($decisionRow['decision'] ?? ''),
'notes' => (string)($decisionRow['notes'] ?? ''),
'subject' => $subject,
'scores' => [
'fall_score' => $context['fall_score'],
'spring_score' => $context['spring_score'],
'year_score' => $context['year_score'],
],
'all_semesters' => $context['all_semesters'],
];
if (trim($htmlInput) !== '') {
$payload['html'] = $htmlInput;
}
Events::trigger('below60.decision_email', $payload);
$query = http_build_query([
'semester' => 'year',
'school_year' => $schoolYear,
]);
return redirect()
->to(base_url('grading/below-60/decisions') . '?' . $query)
->with('status', 'Decision email sent to parent(s).');
}
private function getBelowSixtyDecisionEmailContext(int $studentId, string $schoolYear): array
{
$student = $this->db->table('students s')
->select([
's.id',
's.firstname',
's.lastname',
's.is_active',
])
->where('s.id', $studentId)
->get()
->getRowArray();
/*
* Do not require is_active = 1 here.
* You were getting "Student not found" even though the student ID exists.
* If the record exists, let the email preview work.
*/
if (!$student) {
return [
'student' => null,
'decision_row' => null,
'student_name' => '',
'class_section_name' => '',
'fall_score' => null,
'spring_score' => null,
'year_score' => null,
'all_semesters' => [],
];
}
$studentName = trim((string)($student['firstname'] ?? '') . ' ' . (string)($student['lastname'] ?? ''));
if ($studentName === '') {
$studentName = 'Student';
}
$decisionRow = $this->db->table('below_sixty_decisions')
->where('student_id', $studentId)
->where('school_year', $schoolYear)
->where('semester', 'year')
->get()
->getRowArray();
$scoreRows = $this->db->table('semester_scores ss')
->select([
'LOWER(TRIM(ss.semester)) AS sem_key',
'ss.semester',
'ss.semester_score',
'ss.class_section_id',
'cs.class_section_name',
])
->join('classSection cs', 'cs.class_section_id = ss.class_section_id', 'left')
->where('ss.student_id', $studentId)
->where('ss.school_year', $schoolYear)
->whereIn('LOWER(TRIM(ss.semester))', ['fall', 'spring'])
->where('ss.semester_score IS NOT NULL', null, false)
->orderBy('ss.updated_at', 'DESC')
->orderBy('ss.id', 'DESC')
->get()
->getResultArray();
$fallScore = null;
$springScore = null;
$classSectionName = '';
foreach ($scoreRows as $sr) {
$semKey = strtolower(trim((string)($sr['sem_key'] ?? '')));
$score = is_numeric($sr['semester_score'] ?? null) ? (float)$sr['semester_score'] : null;
if ($classSectionName === '' && !empty($sr['class_section_name'])) {
$classSectionName = (string)$sr['class_section_name'];
}
if ($score === null) {
continue;
}
if ($semKey === 'fall' && $fallScore === null) {
$fallScore = $score;
}
if ($semKey === 'spring' && $springScore === null) {
$springScore = $score;
}
}
if ($classSectionName === '') {
$enrollment = $this->db->table('student_class sc')
->select('cs.class_section_name')
->join('classSection cs', 'cs.class_section_id = sc.class_section_id', 'left')
->where('sc.student_id', $studentId)
->where('sc.school_year', $schoolYear)
->orderBy('sc.id', 'DESC')
->get()
->getRowArray();
$classSectionName = (string)($enrollment['class_section_name'] ?? '');
}
if ($fallScore !== null && $springScore !== null) {
$yearScore = round(($fallScore + $springScore) / 2, 2);
} elseif ($fallScore !== null) {
$yearScore = round($fallScore, 2);
} elseif ($springScore !== null) {
$yearScore = round($springScore, 2);
} else {
$yearScore = null;
}
return [
'student' => $student,
'decision_row' => $decisionRow,
'student_name' => $studentName,
'class_section_name' => $classSectionName,
'fall_score' => $fallScore,
'spring_score' => $springScore,
'year_score' => $yearScore,
'all_semesters' => $this->fetchAllSemestersForStudent($studentId, $schoolYear),
];
}
public function previewDecisionEmail() public function previewDecisionEmail()
{ {
$studentId = (int)$this->request->getGet('student_id'); $studentId = (int)$this->request->getGet('student_id');
+113 -17
View File
@@ -317,18 +317,27 @@ class ReportCardsController extends PrintablesBaseController
$examCommentTypes = $isSecond $examCommentTypes = $isSecond
? ['final'] ? ['final']
: ($isFirst ? ['midterm'] : ['midterm', 'final']); : ($isFirst ? ['midterm'] : ['midterm', 'final']);
$commentTypes = array_merge($examCommentTypes, ['ptap', 'attendance', 'attendance_comment']); $wantedCommentTypes = array_merge($examCommentTypes, ['ptap', 'attendance']);
$hasCommentReview = $this->db->fieldExists('comment_review', 'score_comments'); $hasCommentReview = $this->db->fieldExists('comment_review', 'score_comments');
$commentSelect = $hasCommentReview $hasCommentSemester = $this->db->fieldExists('semester', 'score_comments');
? 'student_id, score_type, comment, comment_review' $hasCommentUpdatedAt = $this->db->fieldExists('updated_at', 'score_comments');
: 'student_id, score_type, comment'; $commentSelectParts = ['student_id', 'score_type', 'comment'];
if ($hasCommentReview) {
$commentSelectParts[] = 'comment_review';
}
if ($hasCommentSemester) {
$commentSelectParts[] = 'semester';
}
// Do NOT filter comments by score_type or semester here.
// A single bad row like "Attendance Comment", "attendance-comment", or a blank semester
// should not make one student look incomplete while the PDF can still print the comment.
$commentBuilder = $this->scoreCommentModel $commentBuilder = $this->scoreCommentModel
->select($commentSelect) ->select(implode(', ', $commentSelectParts))
->whereIn('student_id', $studentIds) ->whereIn('student_id', $studentIds)
->where('school_year', $year) ->where('school_year', $year);
->whereIn('score_type', $commentTypes); if ($hasCommentUpdatedAt) {
if ($sem !== '') { $commentBuilder->orderBy('updated_at', 'DESC');
$this->applySemesterFilter($commentBuilder, $sem, 'semester');
} }
$commentRows = []; $commentRows = [];
try { try {
@@ -336,25 +345,92 @@ class ReportCardsController extends PrintablesBaseController
} catch (\Throwable $e) { } catch (\Throwable $e) {
} }
$cleanComment = static function ($value): string {
$text = (string)($value ?? '');
$text = str_replace("\xc2\xa0", ' ', $text); // normalize non-breaking spaces
return trim($text);
};
$normalizeCommentType = static function ($value) use ($cleanComment): string {
$type = strtolower($cleanComment($value));
$type = preg_replace('/[^a-z0-9]+/', '_', $type);
$type = trim((string)$type, '_');
$map = [
'attendance_comment' => 'attendance',
'attendance_comments' => 'attendance',
'attendance' => 'attendance',
'attendence' => 'attendance',
'attendence_comment' => 'attendance',
'attendence_comments' => 'attendance',
'ptap_comment' => 'ptap',
'ptap_comments' => 'ptap',
'ptap' => 'ptap',
'midterm_comment' => 'midterm',
'midterm_comments' => 'midterm',
'midterm' => 'midterm',
'final_comment' => 'final',
'final_comments' => 'final',
'final' => 'final',
];
return $map[$type] ?? $type;
};
$normalizeSemester = static function ($value) use ($cleanComment): string {
$v = strtolower($cleanComment($value));
$v = preg_replace('/[^a-z0-9]+/', ' ', $v);
$v = trim((string)$v);
if (in_array($v, ['fall', 'first', 'first semester', 'semester 1', '1'], true)) {
return 'fall';
}
if (in_array($v, ['spring', 'second', 'second semester', 'semester 2', '2'], true)) {
return 'spring';
}
return $v;
};
$wantedSemester = $normalizeSemester($sem);
$commentsByStudent = []; $commentsByStudent = [];
$commentPriorityByStudent = [];
foreach ($commentRows as $row) { foreach ($commentRows as $row) {
$sid = (int)($row['student_id'] ?? 0); $sid = (int)($row['student_id'] ?? 0);
if ($sid <= 0) { if ($sid <= 0) {
continue; continue;
} }
$typeRaw = strtolower(trim((string)($row['score_type'] ?? '')));
if ($typeRaw === '') { $typeRaw = $normalizeCommentType($row['score_type'] ?? '');
if ($typeRaw === '' || !in_array($typeRaw, $wantedCommentTypes, true)) {
continue; continue;
} }
if ($typeRaw === 'attendance_comment') {
$typeRaw = 'attendance'; $rawVal = $cleanComment($row['comment'] ?? '');
if ($typeRaw === 'attendance') {
// Attendance comments are stored in score_comments.comment.
// Do not use comment_review here, because it can be blank while the PDF comment exists.
$commentVal = $rawVal;
} else {
// For non-attendance comments, prefer reviewed text but fall back to the saved comment.
$reviewVal = $hasCommentReview ? $cleanComment($row['comment_review'] ?? '') : '';
$commentVal = $reviewVal !== '' ? $reviewVal : $rawVal;
} }
$reviewVal = $hasCommentReview ? trim((string)($row['comment_review'] ?? '')) : '';
$commentVal = $hasCommentReview ? $reviewVal : trim((string)($row['comment'] ?? ''));
if ($commentVal === '') { if ($commentVal === '') {
continue; continue;
} }
$rowSemester = $hasCommentSemester ? $normalizeSemester($row['semester'] ?? '') : '';
$priority = 1;
if ($wantedSemester !== '' && $rowSemester === $wantedSemester) {
$priority = 3;
} elseif ($rowSemester === '') {
$priority = 2;
}
$existingPriority = $commentPriorityByStudent[$sid][$typeRaw] ?? 0;
if (!isset($commentsByStudent[$sid][$typeRaw]) || $priority >= $existingPriority) {
$commentsByStudent[$sid][$typeRaw] = $commentVal; $commentsByStudent[$sid][$typeRaw] = $commentVal;
$commentPriorityByStudent[$sid][$typeRaw] = $priority;
}
} }
$isNumeric = static fn($v) => $v !== null && $v !== '' && is_numeric($v); $isNumeric = static fn($v) => $v !== null && $v !== '' && is_numeric($v);
@@ -467,6 +543,20 @@ class ReportCardsController extends PrintablesBaseController
if (trim((string)($commentSet['ptap'] ?? '')) === '') { if (trim((string)($commentSet['ptap'] ?? '')) === '') {
$missing[] = 'PTAP comment'; $missing[] = 'PTAP comment';
} }
// Keep completeness consistent with the PDF report generation:
// the report can auto-generate an attendance comment from attendance_score.
if (trim((string)($commentSet['attendance'] ?? '')) === '' && $isNumeric($attendanceScore)) {
$autoAttendance = attendance_comment_from_score(
(float)$attendanceScore,
trim((string)($student['firstname'] ?? ''))
);
if ($autoAttendance !== null && trim((string)$autoAttendance) !== '') {
$commentSet['attendance'] = $autoAttendance;
$warnings[] = 'Attendance comment computed from attendance score';
}
}
if (trim((string)($commentSet['attendance'] ?? '')) === '') { if (trim((string)($commentSet['attendance'] ?? '')) === '') {
$missing[] = 'Attendance comment'; $missing[] = 'Attendance comment';
} }
@@ -1531,10 +1621,16 @@ $scoresEndY = $pdf->GetY();
if ($typeRaw === 'attendance_comment') { if ($typeRaw === 'attendance_comment') {
$typeRaw = 'attendance'; $typeRaw = 'attendance';
} }
$rawComment = trim((string)($row['comment'] ?? ''));
if ($typeRaw === 'attendance') {
// Attendance comments must come from score_comments.comment.
$commentVal = $rawComment;
} else {
$reviewVal = trim((string)($row['comment_review'] ?? '')); $reviewVal = trim((string)($row['comment_review'] ?? ''));
$commentVal = $this->db->fieldExists('comment_review', 'score_comments') $commentVal = $this->db->fieldExists('comment_review', 'score_comments') && $reviewVal !== ''
? $reviewVal ? $reviewVal
: trim((string)($row['comment'] ?? '')); : $rawComment;
}
if ($commentVal === '') { if ($commentVal === '') {
continue; continue;
} }
+73 -40
View File
@@ -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');
+60 -47
View File
@@ -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 &amp; Decision</th> <th style="min-width:300px">Comments / Rationale &amp; 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>
File diff suppressed because it is too large Load Diff