fix decisions and rank

This commit is contained in:
root
2026-05-30 03:19:10 -04:00
parent fcfa56b3f5
commit 9ee75fe4cc
8 changed files with 1552 additions and 901 deletions
+406 -187
View File
@@ -38,11 +38,14 @@ foreach ($statsPerClass as $csid => $cs) {
if (!isset($gradeGroups[$sortKey])) {
$gradeGroups[$sortKey] = ['label' => $label, 'slug' => $slug, 'csids' => []];
}
$gradeGroups[$sortKey]['csids'][] = $csid;
}
ksort($gradeGroups);
$gradeKeys = array_keys($gradeGroups);
$defaultKey = $gradeKeys[0] ?? null;
$gradeKeys = array_keys($gradeGroups);
$defaultKey = $gradeKeys[0] ?? null;
$decisionBadge = [
'Pass' => 'success',
@@ -54,14 +57,20 @@ $decisionBadge = [
];
?>
<h2 class="text-center mt-4 mb-3"><i class="bi bi-award me-2"></i>Generate Certificates</h2>
<h2 class="text-center mt-4 mb-3">
<i class="bi bi-award me-2"></i>Generate Certificates
</h2>
<!-- School year filter -->
<div class="d-flex justify-content-end mb-3">
<form method="get" action="<?= site_url('administrator/certificates') ?>" class="d-flex gap-2 align-items-center">
<label class="form-label mb-0 me-1 text-muted small">School Year</label>
<input type="text" name="school_year" class="form-control form-control-sm" style="width:130px;"
value="<?= esc($schoolYear) ?>" placeholder="e.g. 2024-2025">
<input type="text"
name="school_year"
class="form-control form-control-sm"
style="width:130px;"
value="<?= esc($schoolYear) ?>"
placeholder="e.g. 2024-2025">
<button type="submit" class="btn btn-sm btn-outline-primary">
<i class="bi bi-arrow-repeat me-1"></i>Reload
</button>
@@ -82,167 +91,228 @@ $decisionBadge = [
<!-- Grade tabs -->
<ul class="nav nav-tabs justify-content-center" id="certTabs" role="tablist" style="flex-wrap:wrap;row-gap:.25rem;">
<?php foreach ($gradeGroups as $key => $group): ?>
<?php
$slug = $group['slug'];
$label = $group['label'];
$total = array_sum(array_map(fn($id) => $statsPerClass[$id]['total'] ?? 0, $group['csids']));
$grpPass = array_sum(array_map(fn($id) => $statsPerClass[$id]['pass'] ?? 0, $group['csids']));
$grpCert = array_sum(array_map(fn($id) => $statsPerClass[$id]['cert'] ?? 0, $group['csids']));
$fullyDone = $grpPass > 0 && $grpCert >= $grpPass;
$hasPass = $grpPass > 0;
$isActive = ($key === $defaultKey);
$statusTitle = $hasPass
? ($fullyDone ? 'Fully generated (' . $grpCert . '/' . $grpPass . ')' : 'Not fully generated (' . $grpCert . '/' . $grpPass . ')')
: 'No eligible students';
?>
<li class="nav-item" role="presentation">
<a class="nav-link <?= $isActive ? 'active' : '' ?>"
id="cert-<?= esc($slug) ?>-tab"
data-bs-toggle="tab"
href="#cert-<?= esc($slug) ?>"
role="tab"
aria-controls="cert-<?= esc($slug) ?>"
aria-selected="<?= $isActive ? 'true' : 'false' ?>">
<span class="cert-status-dot <?= !$hasPass ? 'no-eligible' : ($fullyDone ? 'done' : 'pending') ?>"
title="<?= esc($statusTitle) ?>"></span>
<?= esc($label) ?>
<span class="badge bg-secondary ms-1"><?= $total ?></span>
</a>
</li>
<?php
$slug = $group['slug'];
$label = $group['label'];
$total = array_sum(array_map(fn($id) => $statsPerClass[$id]['total'] ?? 0, $group['csids']));
$grpPass = array_sum(array_map(fn($id) => $statsPerClass[$id]['pass'] ?? 0, $group['csids']));
$grpCert = array_sum(array_map(fn($id) => $statsPerClass[$id]['cert'] ?? 0, $group['csids']));
$fullyDone = $grpPass > 0 && $grpCert >= $grpPass;
$hasPass = $grpPass > 0;
$isActive = ($key === $defaultKey);
$statusTitle = $hasPass
? ($fullyDone ? 'Fully generated (' . $grpCert . '/' . $grpPass . ')' : 'Not fully generated (' . $grpCert . '/' . $grpPass . ')')
: 'No eligible students';
?>
<li class="nav-item" role="presentation">
<a class="nav-link <?= $isActive ? 'active' : '' ?>"
id="cert-<?= esc($slug) ?>-tab"
data-bs-toggle="tab"
href="#cert-<?= esc($slug) ?>"
role="tab"
aria-controls="cert-<?= esc($slug) ?>"
aria-selected="<?= $isActive ? 'true' : 'false' ?>">
<span class="cert-status-dot <?= !$hasPass ? 'no-eligible' : ($fullyDone ? 'done' : 'pending') ?>"
title="<?= esc($statusTitle) ?>"></span>
<?= esc($label) ?>
<span class="badge bg-secondary ms-1"><?= $total ?></span>
</a>
</li>
<?php endforeach; ?>
</ul>
<!-- Tab content -->
<div class="tab-content mt-3" id="certTabContent">
<?php foreach ($gradeGroups as $key => $group): ?>
<?php $isActive = ($key === $defaultKey); ?>
<div class="tab-pane fade <?= $isActive ? 'show active' : '' ?>"
id="cert-<?= esc($group['slug']) ?>"
role="tabpanel"
aria-labelledby="cert-<?= esc($group['slug']) ?>-tab">
<?php $isActive = ($key === $defaultKey); ?>
<?php foreach ($group['csids'] as $csid): ?>
<?php
$cs = $statsPerClass[$csid];
$students = $studentsByClass[$csid] ?? [];
$csPass = $cs['pass'];
$csCert = $cs['cert'];
$csRemain = max(0, $csPass - $csCert);
$formId = 'certForm-' . $csid;
$tableId = 'studentsTable-' . $csid;
?>
<div class="tab-pane fade <?= $isActive ? 'show active' : '' ?>"
id="cert-<?= esc($group['slug']) ?>"
role="tabpanel"
aria-labelledby="cert-<?= esc($group['slug']) ?>-tab">
<h4 class="mt-4 mb-2 text-center"><?= esc($cs['name']) ?></h4>
<?php foreach ($group['csids'] as $csid): ?>
<?php
$cs = $statsPerClass[$csid];
$students = $studentsByClass[$csid] ?? [];
$csPass = $cs['pass'];
$csCert = $cs['cert'];
$csRemain = max(0, $csPass - $csCert);
$formId = 'certForm-' . $csid;
$tableId = 'studentsTable-' . $csid;
?>
<?php if (empty($students)): ?>
<p class="text-muted text-center">No active students.</p>
<?php else: ?>
<h4 class="mt-4 mb-2 text-center"><?= esc($cs['name']) ?></h4>
<form method="post" action="<?= site_url('administrator/certificates/generate') ?>"
id="<?= esc($formId) ?>" class="cert-form mb-5">
<?= csrf_field() ?>
<input type="hidden" name="class_section_id" value="<?= (int)$csid ?>">
<input type="hidden" name="school_year" value="<?= esc($schoolYear) ?>">
<?php if (empty($students)): ?>
<p class="text-muted text-center">No active students.</p>
<?php else: ?>
<!-- Stats + date picker -->
<div class="d-flex justify-content-between align-items-center flex-wrap gap-2 mb-2">
<div class="d-flex align-items-center gap-4 flex-wrap">
<span class="fw-semibold">
Students <span class="badge bg-secondary ms-1"><?= count($students) ?></span>
</span>
<span class="text-muted small">
<strong class="text-success"><?= $csPass ?></strong> Pass
</span>
<span class="text-muted small">
<strong class="text-primary"><?= $csCert ?></strong> Generated
</span>
<span class="text-muted small">
<strong class="<?= $csRemain > 0 ? 'text-warning' : 'text-muted' ?>"><?= $csRemain ?></strong> Remaining
</span>
</div>
<div class="input-group input-group-sm" style="width:200px;">
<span class="input-group-text"><i class="bi bi-calendar3"></i></span>
<input type="date" class="form-control cert-date-picker"
value="<?= date('Y-m-d') ?>" title="Certificate date">
<input type="hidden" name="cert_date" class="cert-date-hidden" value="<?= esc($certDate) ?>">
</div>
</div>
<form method="post"
action="<?= site_url('administrator/certificates/generate') ?>"
id="<?= esc($formId) ?>"
class="cert-form mb-5">
<?= csrf_field() ?>
<!-- Student table -->
<div class="table-responsive">
<table class="table table-hover table-striped align-middle mb-0 cert-students-table"
id="<?= esc($tableId) ?>">
<thead class="table-light">
<tr>
<th style="width:40px;" class="text-center">
<input class="form-check-input cert-select-all" type="checkbox">
</th>
<th>First Name</th>
<th>Last Name</th>
<th>Decision</th>
<th>Certificate No.</th>
</tr>
</thead>
<tbody>
<?php foreach ($students as $s): ?>
<?php
$sid = (int)$s['student_id'];
$stuDec = $decisionsByStudent[$sid] ?? [];
$certNo = $certsByStudent[$sid] ?? null;
$allDecs = array_map(fn($d) => $d['decision'], $stuDec);
$hasPending = in_array('', $allDecs, true);
$unique = array_values(array_unique(array_filter($allDecs, fn($d) => $d !== '')));
$isPass = !empty($stuDec) && !$hasPending && $unique === ['Pass'];
$displayDecs = $isPass ? ['Pass'] : array_values(array_filter($unique, fn($d) => $d !== 'Pass'));
?>
<tr>
<td class="text-center">
<input class="form-check-input cert-student-check" type="checkbox"
name="student_ids[]" value="<?= $sid ?>"
<?= $isPass ? '' : 'disabled' ?>>
</td>
<td><?= esc($s['firstname']) ?></td>
<td><?= esc($s['lastname']) ?></td>
<td>
<?php if (empty($stuDec)): ?>
<span class="text-muted small">—</span>
<?php elseif ($hasPending || empty($unique) || empty($displayDecs)): ?>
<span class="badge bg-warning text-dark">Pending</span>
<?php else: ?>
<?php foreach ($displayDecs as $dec): $color = $decisionBadge[$dec] ?? 'secondary'; ?>
<span class="badge bg-<?= esc($color) ?> me-1"><?= esc($dec) ?></span>
<input type="hidden" name="class_section_id" value="<?= (int)$csid ?>">
<input type="hidden" name="school_year" value="<?= esc($schoolYear) ?>">
<!-- Stats + date picker -->
<div class="d-flex justify-content-between align-items-center flex-wrap gap-2 mb-2">
<div class="d-flex align-items-center gap-4 flex-wrap">
<span class="fw-semibold">
Students <span class="badge bg-secondary ms-1"><?= count($students) ?></span>
</span>
<span class="text-muted small">
<strong class="text-success"><?= $csPass ?></strong> Pass
</span>
<span class="text-muted small">
<strong class="text-primary"><?= $csCert ?></strong> Generated
</span>
<span class="text-muted small">
<strong class="<?= $csRemain > 0 ? 'text-warning' : 'text-muted' ?>">
<?= $csRemain ?>
</strong>
Remaining
</span>
</div>
<div class="input-group input-group-sm" style="width:200px;">
<span class="input-group-text"><i class="bi bi-calendar3"></i></span>
<input type="date"
class="form-control cert-date-picker"
value="<?= date('Y-m-d') ?>"
title="Certificate date">
<input type="hidden"
name="cert_date"
class="cert-date-hidden"
value="<?= esc($certDate) ?>">
</div>
</div>
<!-- Student table -->
<div class="table-responsive">
<table class="table table-hover table-striped align-middle mb-0 cert-students-table"
id="<?= esc($tableId) ?>">
<thead class="table-light">
<tr>
<th style="width:40px;" class="text-center">
<input class="form-check-input cert-select-all" type="checkbox">
</th>
<th>First Name</th>
<th>Last Name</th>
<th class="text-center">Year Score</th>
<th>Decision</th>
<th>Certificate No.</th>
</tr>
</thead>
<tbody>
<?php foreach ($students as $s): ?>
<?php
$sid = (int)$s['student_id'];
$stuDec = $decisionsByStudent[$sid] ?? [];
$certNo = $certsByStudent[$sid] ?? null;
$allDecs = array_map(
fn($d) => trim((string)($d['decision'] ?? '')),
$stuDec
);
$hasPending = in_array('', $allDecs, true);
$unique = array_values(array_unique(array_filter(
$allDecs,
fn($d) => $d !== ''
)));
$isPass = !empty($stuDec) && !$hasPending && $unique === ['Pass'];
$displayDecs = $isPass
? ['Pass']
: array_values(array_filter($unique, fn($d) => $d !== 'Pass'));
$yearScore = null;
foreach ($stuDec as $d) {
if (
array_key_exists('year_score', $d)
&& $d['year_score'] !== null
&& $d['year_score'] !== ''
) {
$yearScore = $d['year_score'];
break;
}
}
$fmtYearScore = is_numeric($yearScore)
? number_format((float)$yearScore, 2)
: '—';
?>
<tr>
<td class="text-center">
<input class="form-check-input cert-student-check"
type="checkbox"
name="student_ids[]"
value="<?= $sid ?>"
<?= $isPass ? '' : 'disabled' ?>>
</td>
<td><?= esc($s['firstname']) ?></td>
<td><?= esc($s['lastname']) ?></td>
<td class="text-center fw-semibold">
<?= esc($fmtYearScore) ?>
</td>
<td>
<?php if (empty($stuDec)): ?>
<span class="text-muted small">—</span>
<?php elseif ($hasPending || empty($unique) || empty($displayDecs)): ?>
<span class="badge bg-warning text-dark">Pending</span>
<?php else: ?>
<?php foreach ($displayDecs as $dec): ?>
<?php $color = $decisionBadge[$dec] ?? 'secondary'; ?>
<span class="badge bg-<?= esc($color) ?> me-1">
<?= esc($dec) ?>
</span>
<?php endforeach; ?>
<?php endif; ?>
</td>
<td>
<?php if ($certNo): ?>
<a href="<?= site_url('administrator/certificates/reprint/' . rawurlencode($certNo)) ?>"
target="_blank"
class="font-monospace small">
<?= esc($certNo) ?>
</a>
<?php else: ?>
<span class="text-muted">—</span>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</td>
<td>
<?php if ($certNo): ?>
<a href="<?= site_url('administrator/certificates/reprint/' . rawurlencode($certNo)) ?>"
target="_blank" class="font-monospace small">
<?= esc($certNo) ?>
</a>
<?php else: ?>
<span class="text-muted">—</span>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</tbody>
</table>
</div>
<!-- Footer -->
<div class="d-flex justify-content-between align-items-center mt-2">
<span class="text-muted small cert-selected-count">0 students selected</span>
<button type="submit" class="btn btn-success cert-generate-btn" disabled>
<i class="bi bi-printer me-1"></i>Generate &amp; Print Certificates
</button>
</div>
</form>
<!-- Footer -->
<div class="d-flex justify-content-between align-items-center mt-2">
<span class="text-muted small cert-selected-count">0 students selected</span>
<button type="submit" class="btn btn-success cert-generate-btn" disabled>
<i class="bi bi-printer me-1"></i>Generate &amp; Print Certificates
</button>
</div>
</form>
<?php endif; ?>
<?php endforeach; ?>
<?php endif; ?>
<?php endforeach; ?>
</div>
</div>
<?php endforeach; ?>
</div>
@@ -253,44 +323,102 @@ $decisionBadge = [
<script>
(function () {
const csrfRefreshUrl = <?= json_encode(site_url('administrator/certificates/csrf-token'), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT) ?>;
let currentCsrfTokenName = <?= json_encode(csrf_token(), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT) ?>;
const csrfRefreshUrl = <?= json_encode(site_url('administrator/certificates/csrf-token'), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT) ?>;
let currentCsrfTokenName = <?= json_encode(csrf_token(), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT) ?>;
function cssEscape(v) {
return window.CSS?.escape ? window.CSS.escape(v) : String(v).replace(/(["\\.#:[\],= ])/g, '\\$1');
}
function updateCsrfInForm(form, name, hash) {
if (!form || !name || !hash) return;
form.querySelectorAll('input[type="hidden"]').forEach(inp => {
if (inp.name === name || inp.name === currentCsrfTokenName) { inp.name = name; inp.value = hash; }
if (inp.name === name || inp.name === currentCsrfTokenName) {
inp.name = name;
inp.value = hash;
}
});
let inp = form.querySelector(`input[name="${cssEscape(name)}"]`);
if (!inp) { inp = document.createElement('input'); inp.type = 'hidden'; inp.name = name; form.appendChild(inp); }
if (!inp) {
inp = document.createElement('input');
inp.type = 'hidden';
inp.name = name;
form.appendChild(inp);
}
inp.value = hash;
currentCsrfTokenName = name;
}
async function refreshCsrf(form) {
const r = await fetch(csrfRefreshUrl, { method: 'GET', credentials: 'same-origin', headers: { 'X-Requested-With': 'XMLHttpRequest', 'Cache-Control': 'no-store' } });
const r = await fetch(csrfRefreshUrl, {
method: 'GET',
credentials: 'same-origin',
headers: {
'X-Requested-With': 'XMLHttpRequest',
'Cache-Control': 'no-store'
}
});
if (!r.ok) throw new Error('CSRF refresh failed');
const d = await r.json();
if (d?.csrf_token && d?.csrf_hash) updateCsrfInForm(form, d.csrf_token, d.csrf_hash);
if (d?.csrf_token && d?.csrf_hash) {
updateCsrfInForm(form, d.csrf_token, d.csrf_hash);
}
}
let pdfWindow = null, activePdfBlobUrl = null;
let pdfWindow = null;
let activePdfBlobUrl = null;
function openPdfWindow() {
if (!pdfWindow || pdfWindow.closed) pdfWindow = window.open('', 'certificatePdfWindow');
if (!pdfWindow || pdfWindow.closed) {
pdfWindow = window.open('', 'certificatePdfWindow');
}
if (!pdfWindow) return null;
pdfWindow.document.open();
pdfWindow.document.write(`<!DOCTYPE html><html><head><meta charset="utf-8"><title>Certificate PDF</title>
<style>html,body{margin:0;height:100%;background:#f3f4f6;font-family:Arial,sans-serif}.viewer-shell{display:flex;flex-direction:column;height:100%}.viewer-status{padding:12px 16px;background:#111827;color:#fff;font-size:14px}.viewer-frame{flex:1;width:100%;border:0;background:#cbd5e1}</style></head>
<body><div class="viewer-shell"><div class="viewer-status" id="viewerStatus">Preparing certificate PDF...</div><iframe class="viewer-frame" id="pdfFrame"></iframe></div>
<script>window.showCertificatePdf=function(url,fn){const f=document.getElementById('pdfFrame'),s=document.getElementById('viewerStatus');if(s)s.textContent=fn?'Showing '+fn:'Certificate PDF ready';if(f)f.src=url;document.title=fn||'Certificate PDF'};<\/script></body></html>`);
<style>
html,body{margin:0;height:100%;background:#f3f4f6;font-family:Arial,sans-serif}
.viewer-shell{display:flex;flex-direction:column;height:100%}
.viewer-status{padding:12px 16px;background:#111827;color:#fff;font-size:14px}
.viewer-frame{flex:1;width:100%;border:0;background:#cbd5e1}
</style></head>
<body>
<div class="viewer-shell">
<div class="viewer-status" id="viewerStatus">Preparing certificate PDF...</div>
<iframe class="viewer-frame" id="pdfFrame"></iframe>
</div>
<script>
window.showCertificatePdf=function(url,fn){
const f=document.getElementById('pdfFrame'),s=document.getElementById('viewerStatus');
if(s)s.textContent=fn?'Showing '+fn:'Certificate PDF ready';
if(f)f.src=url;
document.title=fn||'Certificate PDF';
};
<\/script>
</body></html>`);
pdfWindow.document.close();
return pdfWindow;
}
function showError(pane, msg) {
let el = pane.querySelector('.cert-inline-error');
if (!el) { el = document.createElement('div'); el.className = 'alert alert-danger alert-dismissible fade show cert-inline-error'; pane.prepend(el); }
if (!el) {
el = document.createElement('div');
el.className = 'alert alert-danger alert-dismissible fade show cert-inline-error';
pane.prepend(el);
}
el.innerHTML = `${msg}<button type="button" class="btn-close" data-bs-dismiss="alert"></button>`;
}
@@ -307,62 +435,134 @@ $decisionBadge = [
if (datePicker && dateHidden) {
datePicker.addEventListener('change', function () {
const d = new Date(this.value + 'T00:00:00');
if (!isNaN(d)) dateHidden.value = String(d.getMonth()+1).padStart(2,'0')+'/'+String(d.getDate()).padStart(2,'0')+'/'+d.getFullYear();
if (!isNaN(d)) {
dateHidden.value =
String(d.getMonth() + 1).padStart(2, '0') + '/' +
String(d.getDate()).padStart(2, '0') + '/' +
d.getFullYear();
}
});
}
function updateState() {
const chosen = form.querySelectorAll('.cert-student-check:checked').length;
if (selectedCount) selectedCount.textContent = chosen + ' student' + (chosen !== 1 ? 's' : '') + ' selected';
if (generateBtn) generateBtn.disabled = chosen === 0;
if (selectedCount) {
selectedCount.textContent = chosen + ' student' + (chosen !== 1 ? 's' : '') + ' selected';
}
if (generateBtn) {
generateBtn.disabled = chosen === 0;
}
if (selectAll) {
const ec = eligibleChecks.length;
selectAll.checked = ec > 0 && chosen === ec;
selectAll.checked = ec > 0 && chosen === ec;
selectAll.indeterminate = chosen > 0 && chosen < ec;
}
}
if (selectAll) {
selectAll.addEventListener('change', function () {
eligibleChecks.forEach(c => { c.checked = this.checked; });
eligibleChecks.forEach(c => {
c.checked = this.checked;
});
updateState();
});
}
checks.forEach(c => c.addEventListener('change', updateState));
updateState();
form.addEventListener('submit', async function (e) {
e.preventDefault();
const chosen = form.querySelectorAll('.cert-student-check:checked').length;
if (chosen === 0) { showError(pane, 'Please select at least one student.'); return; }
if (chosen === 0) {
showError(pane, 'Please select at least one student.');
return;
}
const win = openPdfWindow();
if (!win) { showError(pane, 'Unable to open PDF tab — please allow pop-ups.'); return; }
if (!win) {
showError(pane, 'Unable to open PDF tab — please allow pop-ups.');
return;
}
const origHtml = generateBtn.innerHTML;
generateBtn.disabled = true;
generateBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Generating...';
try {
await refreshCsrf(form);
const csrfVal = form.querySelector(`input[name="${cssEscape(currentCsrfTokenName)}"]`)?.value || '';
const fd = new FormData(form);
if (csrfVal) fd.set(currentCsrfTokenName, csrfVal);
const resp = await fetch(form.action, { method: 'POST', body: fd, credentials: 'same-origin', headers: { 'X-Requested-With': 'XMLHttpRequest' } });
const nn = resp.headers.get('X-CSRF-TOKEN-NAME'), nh = resp.headers.get('X-CSRF-TOKEN');
if (nn && nh) updateCsrfInForm(form, nn, nh);
if (csrfVal) {
fd.set(currentCsrfTokenName, csrfVal);
}
const resp = await fetch(form.action, {
method: 'POST',
body: fd,
credentials: 'same-origin',
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
});
const nn = resp.headers.get('X-CSRF-TOKEN-NAME');
const nh = resp.headers.get('X-CSRF-TOKEN');
if (nn && nh) {
updateCsrfInForm(form, nn, nh);
}
const ct = resp.headers.get('Content-Type') || '';
if (!resp.ok || !ct.toLowerCase().includes('application/pdf')) {
let msg = 'Certificate generation failed.';
try { const d = await resp.json(); if (d?.error) msg = d.error; } catch (_) { try { msg = await resp.text() || msg; } catch (_2) {} }
win.close(); showError(pane, msg);
try {
const d = await resp.json();
if (d?.error) {
msg = d.error;
}
} catch (_) {
try {
msg = await resp.text() || msg;
} catch (_2) {}
}
win.close();
showError(pane, msg);
await refreshCsrf(form).catch(() => {});
return;
}
const disp = resp.headers.get('Content-Disposition') || '';
const fn = (disp.match(/filename="?([^"]+)"?/i) || [])[1] || 'Certificates.pdf';
const url = URL.createObjectURL(await resp.blob());
if (activePdfBlobUrl) URL.revokeObjectURL(activePdfBlobUrl);
const fn = (disp.match(/filename="?([^"]+)"?/i) || [])[1] || 'Certificates.pdf';
const url = URL.createObjectURL(await resp.blob());
if (activePdfBlobUrl) {
URL.revokeObjectURL(activePdfBlobUrl);
}
activePdfBlobUrl = url;
win.showCertificatePdf(url, fn);
const activePane = form.closest('.tab-pane');
if (activePane?.id) window.location.hash = activePane.id;
if (activePane?.id) {
window.location.hash = activePane.id;
}
window.location.reload();
} catch (_) {
showError(pane, 'Certificate generation failed. Please try again.');
@@ -376,49 +576,68 @@ $decisionBadge = [
})();
</script>
<?= $this->endSection() ?>
<?= $this->section('scripts') ?>
<script>
// Restore tab from hash — runs after Bootstrap is loaded
(function () {
const hash = window.location.hash;
if (!hash) return;
const tabLink = document.querySelector('a[href="' + hash + '"][data-bs-toggle="tab"]');
if (tabLink && window.bootstrap?.Tab) {
new bootstrap.Tab(tabLink).show();
}
})();
</script>
<style>
.cert-status-dot {
display: inline-block;
width: 8px; height: 8px;
width: 8px;
height: 8px;
border-radius: 50%;
margin-right: 5px;
vertical-align: middle;
flex-shrink: 0;
}
.cert-status-dot.done { background-color: #198754; }
.cert-status-dot.pending { background-color: #dc3545; }
.cert-status-dot.no-eligible { background-color: #adb5bd; }
.cert-status-dot.done {
background-color: #198754;
}
.cert-status-dot.pending {
background-color: #dc3545;
}
.cert-status-dot.no-eligible {
background-color: #adb5bd;
}
</style>
<script>
(function () {
if (!window.$ || !$.fn?.DataTable) return;
$(function () {
document.querySelectorAll('.cert-students-table').forEach(function (tbl) {
if ($.fn.DataTable.isDataTable(tbl)) return;
try {
$(tbl).DataTable({
order: [[1, 'asc'], [2, 'asc']],
pageLength: 100,
lengthMenu: [25, 50, 100, 200],
columnDefs: [{ orderable: false, targets: [0, 3, 4] }]
columnDefs: [
{ orderable: false, targets: [0, 4, 5] }
]
});
} catch (_) {}
});
});
})();
</script>
<?= $this->endSection() ?>
<?= $this->endSection() ?>
<?= $this->endSection() ?>
+294 -183
View File
@@ -11,13 +11,18 @@
<div class="text-muted">
<?= !empty($isYearMode) ? 'Whole Year' : 'Fall' ?> • <?= esc($schoolYear ?? '') ?>
</div>
<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' => 'fall', 'school_year' => $schoolYear ?? ''])) ?>">
Decisions
</a>
<?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)): ?>
<a class="btn btn-outline-secondary btn-sm" href="<?= base_url('grading') ?>">
Back to Grading
@@ -31,14 +36,16 @@
if ($value === null || $value === '') {
return '—';
}
if (is_numeric($value)) {
return esc(number_format((float)$value, 2, '.', ''));
}
return esc($value);
};
// Fall mode displays Fall semester_score.
// Whole Year mode displays the controller-provided annual score: (fall_score + spring_score) / 2.
// Fall mode uses Fall.
// Whole Year mode uses year.
$actionSemester = !empty($isYearMode) ? 'year' : 'fall';
?>
@@ -48,7 +55,9 @@
</div>
<?php else: ?>
<div class="table-responsive below-sixty-table">
<table class="table table-bordered table-striped align-middle w-100 no-mgmt-sticky below-sixty-dt" data-no-mgmt-sticky data-no-dt-fixedheader>
<table class="table table-bordered table-striped align-middle w-100 no-mgmt-sticky below-sixty-dt"
data-no-mgmt-sticky
data-no-dt-fixedheader>
<thead class="table-light">
<tr>
<th>Student Name</th>
@@ -59,12 +68,15 @@
<th>Schedule Meeting</th>
</tr>
</thead>
<tbody>
<?php foreach ($rows as $row): ?>
<?php
$scoreRaw = $row['semester_score'] ?? null;
$scoreVal = is_numeric($scoreRaw) ? (float)$scoreRaw : null;
$scoreClass = '';
if ($scoreVal !== null) {
if ($scoreVal < 50) {
$scoreClass = 'grade-red';
@@ -72,15 +84,19 @@
$scoreClass = 'grade-orange';
}
}
$studentName = trim((string)($row['firstname'] ?? '') . ' ' . (string)($row['lastname'] ?? ''));
$studentName = trim((string)($row['firstname'] ?? '') . ' ' . (string)($row['lastname'] ?? ''));
$studentLabel = $studentName !== '' ? $studentName : 'N/A';
$isClosed = ($row['status'] ?? 'Open') === 'Closed';
$isClosed = ($row['status'] ?? 'Open') === 'Closed';
?>
<tr class="<?= esc($scoreClass) ?>">
<td><?= esc($studentLabel) ?></td>
<td><?= esc($row['class_section_name'] ?? '—') ?></td>
<td class="text-center">
<div class="fw-semibold"><?= $displayScore($scoreRaw) ?></div>
<button type="button"
class="btn btn-outline-secondary btn-xs mt-1 btn-show-details"
style="font-size:0.72rem;padding:1px 7px;"
@@ -90,23 +106,54 @@
Details
</button>
</td>
<td class="text-center">
<form method="post" action="<?= site_url('grading/below-60/status') ?>" class="d-flex align-items-center gap-2 justify-content-center">
<form method="post"
action="<?= site_url('grading/below-60/status') ?>"
class="d-flex align-items-center gap-2 justify-content-center">
<?= csrf_field() ?>
<input type="hidden" name="student_id" value="<?= esc((string)($row['student_id'] ?? '')) ?>">
<input type="hidden" name="semester" value="<?= esc($actionSemester) ?>">
<input type="hidden" name="school_year" value="<?= esc((string)($schoolYear ?? '')) ?>">
<select name="status" class="form-select form-select-sm" style="width: 110px;">
<option value="Open" <?= ($row['status'] ?? 'Open') === 'Open' ? 'selected' : '' ?>>Open</option>
<option value="Closed" <?= ($row['status'] ?? '') === 'Closed' ? 'selected' : '' ?>>Closed</option>
<input type="hidden"
name="student_id"
value="<?= esc((string)($row['student_id'] ?? '')) ?>">
<input type="hidden"
name="semester"
value="<?= esc($actionSemester) ?>">
<input type="hidden"
name="school_year"
value="<?= esc((string)($schoolYear ?? '')) ?>">
<select name="status"
class="form-select form-select-sm"
style="width: 110px;">
<option value="Open" <?= ($row['status'] ?? 'Open') === 'Open' ? 'selected' : '' ?>>
Open
</option>
<option value="Closed" <?= ($row['status'] ?? '') === 'Closed' ? 'selected' : '' ?>>
Closed
</option>
</select>
<input type="text" name="note" class="form-control form-control-sm" style="width: 140px;" placeholder="Note (optional)" value="<?= esc((string)($row['note'] ?? '')) ?>">
<button type="submit" class="btn btn-sm btn-outline-secondary">Update</button>
<input type="text"
name="note"
class="form-control form-control-sm"
style="width: 140px;"
placeholder="Note (optional)"
value="<?= esc((string)($row['note'] ?? '')) ?>">
<button type="submit" class="btn btn-sm btn-outline-secondary">
Update
</button>
</form>
</td>
<td class="text-center">
<?php if ($isClosed): ?>
<button type="button" class="btn btn-sm btn-secondary" disabled>Send Email</button>
<button type="button" class="btn btn-sm btn-secondary" disabled>
Send Email
</button>
<?php else: ?>
<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 ?? ''))) ?>">
@@ -114,9 +161,12 @@
</a>
<?php endif; ?>
</td>
<td class="text-center">
<?php if ($isClosed): ?>
<button class="btn btn-sm btn-secondary" disabled>Schedule</button>
<button class="btn btn-sm btn-secondary" disabled>
Schedule
</button>
<?php else: ?>
<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 ?? ''))) ?>">
@@ -141,13 +191,19 @@
<h5 class="modal-title" id="detailsModalLabel">Score Details</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body" id="detailsModalBody">
<div class="text-center py-4">
<div class="spinner-border text-primary" role="status"></div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Close</button>
<button type="button"
class="btn btn-outline-secondary"
data-bs-dismiss="modal">
Close
</button>
</div>
</div>
</div>
@@ -157,181 +213,236 @@
<?= $this->section('scripts') ?>
<style>
.grade-orange td { background: #fff3cd !important; color: #8a5d00; font-weight: 600; }
.grade-red td { background: #f8d7da !important; color: #842029; font-weight: 600; }
.below-sixty-wrapper { padding-top: 0.75rem; }
.below-sixty-title { position: relative; z-index: 1; margin-bottom: 1.25rem; }
.below-sixty-table { margin-top: 0.75rem; }
.below-sixty-dt td { vertical-align: top; }
.grade-orange td {
background: #fff3cd !important;
color: #8a5d00;
font-weight: 600;
}
.grade-red td {
background: #f8d7da !important;
color: #842029;
font-weight: 600;
}
.below-sixty-wrapper {
padding-top: 0.75rem;
}
.below-sixty-title {
position: relative;
z-index: 1;
margin-bottom: 1.25rem;
}
.below-sixty-table {
margin-top: 0.75rem;
}
.below-sixty-dt td {
vertical-align: top;
}
</style>
<script>
(function() {
function normalizeSemesterFilter() {
const semesterSelect = document.querySelector('select[name="semester"]');
if (!semesterSelect) return;
(function () {
function normalizeSemesterFilter() {
const semesterSelect = document.querySelector('select[name="semester"]');
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';
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) {
$(function () {
const table = $('.below-sixty-dt');
if (!table.length) return;
try {
table.DataTable({
order: [[2, 'asc']],
pageLength: 100,
lengthMenu: [10, 25, 50, 100, 200],
columnDefs: [
{ orderable: false, targets: [3, 4, 5] }
]
});
} catch (_) {}
});
}
const detailsModal = document.getElementById('detailsModal');
const detailsModalBody = document.getElementById('detailsModalBody');
const detailsModalTitle = document.getElementById('detailsModalLabel');
const SCORE_LABELS = {
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'
};
const COMMENT_TYPE_LABELS = {
general: 'General',
attendance: 'Attendance',
attendance_comment: 'Attendance',
midterm: 'Midterm',
final: 'Final Exam',
ptap: 'PTAP'
};
function fmtScore(value) {
if (value === null || value === '' || value === undefined) return '—';
const parsed = parseFloat(value);
return isNaN(parsed) ? value : parsed.toFixed(2);
}
function esc(str) {
return String(str)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
function buildDetailsHtml(semesters) {
if (!semesters || semesters.length === 0) {
return '<div class="alert alert-warning mb-0">No score data found.</div>';
}
document.addEventListener('DOMContentLoaded', normalizeSemesterFilter);
let html = '';
if (window.$ && $.fn && $.fn.DataTable) {
$(function() {
const table = $('.below-sixty-dt');
if (!table.length) return;
try {
table.DataTable({
order: [[2, 'asc']],
pageLength: 100,
lengthMenu: [10, 25, 50, 100, 200],
columnDefs: [{ orderable: false, targets: [3, 4, 5] }]
});
} catch (_) {}
});
}
semesters.forEach(function (sem) {
html += '<h6 class="fw-bold mt-3 mb-2">' + esc(sem.semester || '') + ' Semester';
const detailsModal = document.getElementById('detailsModal');
const detailsModalBody = document.getElementById('detailsModalBody');
const detailsModalTitle = document.getElementById('detailsModalLabel');
const SCORE_LABELS = {
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'
};
const COMMENT_TYPE_LABELS = {
general: 'General',
attendance: 'Attendance',
attendance_comment: 'Attendance',
midterm: 'Midterm',
final: 'Final Exam',
ptap: 'PTAP'
};
function fmtScore(value) {
if (value === null || value === '' || value === undefined) return '—';
const parsed = parseFloat(value);
return isNaN(parsed) ? value : parsed.toFixed(2);
}
function esc(str) {
return String(str)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
function buildDetailsHtml(semesters) {
if (!semesters || semesters.length === 0) {
return '<div class="alert alert-warning mb-0">No score data found.</div>';
if (sem.class_section_name) {
html += ' <span class="text-muted fw-normal fs-6">— ' + esc(sem.class_section_name) + '</span>';
}
let html = '';
semesters.forEach(function(sem) {
html += '<h6 class="fw-bold mt-3 mb-2">' + esc(sem.semester || '') + ' Semester';
if (sem.class_section_name) {
html += ' <span class="text-muted fw-normal fs-6">— ' + esc(sem.class_section_name) + '</span>';
html += '</h6>';
html += '<table class="table table-sm table-bordered mb-2">';
html += '<thead class="table-light"><tr><th>Item</th><th class="text-center">Score</th></tr></thead><tbody>';
let hasScoreRow = false;
Object.entries(SCORE_LABELS).forEach(function (entry) {
const key = entry[0];
const label = entry[1];
const value = sem[key];
if (value === null || value === '' || value === undefined) return;
const bold = key === 'semester_score' ? ' fw-bold' : '';
html += '<tr><td>' + esc(label) + '</td><td class="text-center' + bold + '">' + esc(fmtScore(value)) + '</td></tr>';
hasScoreRow = true;
});
if (!hasScoreRow) {
html += '<tr><td colspan="2" class="text-muted">No scores recorded.</td></tr>';
}
html += '</tbody></table>';
const comments = sem.comments || {};
const commentEntries = Object.entries(comments).filter(function (entry) {
return entry[1] && String(entry[1]).trim();
});
const seen = {};
const deduped = [];
commentEntries.forEach(function (entry) {
const type = entry[0];
const text = String(entry[1]);
const label = COMMENT_TYPE_LABELS[type] || type;
const key = label + '|' + text.trim();
if (!seen[key]) {
seen[key] = true;
deduped.push([label, text]);
}
html += '</h6>';
});
html += '<table class="table table-sm table-bordered mb-2">';
html += '<thead class="table-light"><tr><th>Item</th><th class="text-center">Score</th></tr></thead><tbody>';
if (deduped.length > 0) {
html += '<div class="mb-3">';
html += '<p class="fw-semibold mb-1" style="font-size:0.9rem;">Comments</p>';
let hasScoreRow = false;
Object.entries(SCORE_LABELS).forEach(function(entry) {
const key = entry[0];
const label = entry[1];
const value = sem[key];
if (value === null || value === '' || value === undefined) return;
const bold = key === 'semester_score' ? ' fw-bold' : '';
html += '<tr><td>' + esc(label) + '</td><td class="text-center' + bold + '">' + esc(fmtScore(value)) + '</td></tr>';
hasScoreRow = true;
});
deduped.forEach(function (entry) {
const label = entry[0];
const text = entry[1];
if (!hasScoreRow) {
html += '<tr><td colspan="2" class="text-muted">No scores recorded.</td></tr>';
}
html += '</tbody></table>';
const comments = sem.comments || {};
const commentEntries = Object.entries(comments).filter(function(entry) {
return entry[1] && String(entry[1]).trim();
});
const seen = {};
const deduped = [];
commentEntries.forEach(function(entry) {
const type = entry[0];
const text = String(entry[1]);
const label = COMMENT_TYPE_LABELS[type] || type;
const key = label + '|' + text.trim();
if (!seen[key]) {
seen[key] = true;
deduped.push([label, text]);
}
});
if (deduped.length > 0) {
html += '<div class="mb-3">';
html += '<p class="fw-semibold mb-1" style="font-size:0.9rem;">Comments</p>';
deduped.forEach(function(entry) {
const label = entry[0];
const text = entry[1];
html += '<div class="mb-2 p-2 bg-light rounded border-start border-3 border-secondary">';
html += '<span class="badge bg-secondary me-1" style="font-size:0.7rem;">' + esc(label) + '</span>';
html += '<span class="text-dark" style="font-size:0.9rem;">' + esc(text).replace(/\n/g, '<br>') + '</span>';
html += '</div>';
});
html += '<div class="mb-2 p-2 bg-light rounded border-start border-3 border-secondary">';
html += '<span class="badge bg-secondary me-1" style="font-size:0.7rem;">' + esc(label) + '</span>';
html += '<span class="text-dark" style="font-size:0.9rem;">' + esc(text).replace(/\n/g, '<br>') + '</span>';
html += '</div>';
});
html += '</div>';
}
});
return html;
}
if (detailsModal) {
document.addEventListener('click', function (e) {
const btn = e.target.closest('.btn-show-details');
if (!btn) return;
const studentId = btn.dataset.studentId;
const studentName = btn.dataset.studentName;
const schoolYear = btn.dataset.schoolYear;
detailsModalTitle.textContent = studentName + ' — Score Details';
detailsModalBody.innerHTML = '<div class="text-center py-4"><div class="spinner-border text-primary" role="status"></div></div>';
bootstrap.Modal.getOrCreateInstance(detailsModal).show();
const url = '<?= site_url('grading/below-60/decisions/student-details') ?>'
+ '?student_id=' + encodeURIComponent(studentId)
+ '&school_year=' + encodeURIComponent(schoolYear);
fetch(url, {
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
});
})
.then(function (response) {
return response.json();
})
.then(function (data) {
if (data.error) {
detailsModalBody.innerHTML = '<div class="alert alert-danger">' + esc(data.error) + '</div>';
return;
}
return html;
}
if (detailsModal) {
document.addEventListener('click', function(e) {
const btn = e.target.closest('.btn-show-details');
if (!btn) return;
const studentId = btn.dataset.studentId;
const studentName = btn.dataset.studentName;
const schoolYear = btn.dataset.schoolYear;
detailsModalTitle.textContent = studentName + ' — Score Details';
detailsModalBody.innerHTML = '<div class="text-center py-4"><div class="spinner-border text-primary" role="status"></div></div>';
bootstrap.Modal.getOrCreateInstance(detailsModal).show();
const url = '<?= site_url('grading/below-60/decisions/student-details') ?>'
+ '?student_id=' + encodeURIComponent(studentId)
+ '&school_year=' + encodeURIComponent(schoolYear);
fetch(url, { headers: { 'X-Requested-With': 'XMLHttpRequest' } })
.then(function(response) { return response.json(); })
.then(function(data) {
if (data.error) {
detailsModalBody.innerHTML = '<div class="alert alert-danger">' + esc(data.error) + '</div>';
return;
}
detailsModalBody.innerHTML = buildDetailsHtml(data.semesters);
})
.catch(function() {
detailsModalBody.innerHTML = '<div class="alert alert-danger">Failed to load details.</div>';
});
});
}
})();
detailsModalBody.innerHTML = buildDetailsHtml(data.semesters);
})
.catch(function () {
detailsModalBody.innerHTML = '<div class="alert alert-danger">Failed to load details.</div>';
});
});
}
})();
</script>
<?= $this->endSection() ?>
<?= $this->endSection() ?>
+239 -88
View File
@@ -1,25 +1,37 @@
<?= $this->extend('layout/management_layout') ?>
<?= $this->section('content') ?>
<?php
// This page is Whole Year only.
// Do not expose semester filter here.
$semester = 'year';
?>
<div class="container-fluid">
<div class="wrapper below-sixty-decisions-wrapper">
<h2 class="text-center mt-4 mb-4">Below 60 — Decisions</h2>
<?= $this->include('partials/academic_filter') ?>
<h2 class="text-center mt-4 mb-4">Below 60 — Whole Year Decisions</h2>
<div class="d-flex justify-content-between align-items-center mb-3 flex-wrap gap-2">
<div class="text-muted">
<?= esc(ucfirst($semester ?? '')) ?> • <?= esc($schoolYear ?? '') ?>
Whole Year • <?= esc($schoolYear ?? '') ?>
</div>
<div class="d-flex gap-2">
<div class="d-flex gap-2 flex-wrap">
<a class="btn btn-outline-secondary btn-sm"
href="<?= site_url('grading/below-60?' . http_build_query(['semester' => $semester, 'school_year' => $schoolYear])) ?>">
href="<?= site_url('grading/below-60?' . http_build_query([
'semester' => 'year',
'school_year' => $schoolYear ?? '',
])) ?>">
← Back to Below 60
</a>
<a class="btn btn-outline-primary btn-sm"
href="<?= site_url('grading/decisions?' . http_build_query(['semester' => $semester, 'school_year' => $schoolYear])) ?>">
href="<?= site_url('grading/decisions?' . http_build_query([
'school_year' => $schoolYear ?? '',
])) ?>">
All Decisions
</a>
<?php if (!empty($canViewGrading)): ?>
<a class="btn btn-outline-secondary btn-sm" href="<?= base_url('grading') ?>">
Grading
@@ -34,6 +46,7 @@
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
<?php endif; ?>
<?php if (!empty(session()->getFlashdata('error'))): ?>
<div class="alert alert-danger alert-dismissible fade show" role="alert">
<?= esc(session()->getFlashdata('error')) ?>
@@ -47,7 +60,7 @@
'Pass' => 'Pass',
'Repeat Class' => 'Repeat Class',
'Make-up exam in fall' => 'Make-up exam in fall',
'Deferred decision' => 'Deferred decision',
'Deferred decision' => 'Deferred decision',
'Expel' => 'Expel',
'Withdrawn' => 'Withdrawn',
];
@@ -56,93 +69,136 @@
'Pass' => 'success',
'Repeat Class' => 'danger',
'Make-up exam in fall' => 'info',
'Deferred decision' => 'info',
'Deferred decision' => 'info',
'Expel' => 'danger',
'Withdrawn' => 'secondary',
];
$displayScore = function ($value) {
if ($value === null || $value === '') return '—';
if (is_numeric($value)) return esc(number_format((float)$value, 2, '.', ''));
if ($value === null || $value === '') {
return '—';
}
if (is_numeric($value)) {
return esc(number_format((float)$value, 2, '.', ''));
}
return esc($value);
};
?>
<?php if (empty($rows)): ?>
<div class="alert alert-success text-center d-inline-block">
No students below 60 for this selection.
No students below 60 for the whole year.
</div>
<?php else: ?>
<div class="table-responsive">
<table class="table table-bordered table-striped align-middle w-100 decisions-dt" data-no-mgmt-sticky data-no-dt-fixedheader>
<table class="table table-bordered table-striped align-middle w-100 decisions-dt"
data-no-mgmt-sticky
data-no-dt-fixedheader>
<thead class="table-light">
<tr>
<th style="min-width:160px">Student Name</th>
<th>Section</th>
<th class="text-center">Score</th>
<th class="text-center">Year Score</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:130px" class="text-center">Final Decision</th>
<th style="min-width:130px" class="text-center">Certificate</th>
</tr>
</thead>
<tbody>
<?php foreach ($rows as $row): ?>
<?php
$scoreRaw = $row['semester_score'] ?? null;
$scoreVal = is_numeric($scoreRaw) ? (float)$scoreRaw : null;
$rowClass = '';
// Whole-year score. Prefer year_score if the controller provides it.
// Fall/Spring details remain available through the Details modal.
$scoreRaw = $row['year_score'] ?? $row['semester_score'] ?? null;
$scoreVal = is_numeric($scoreRaw) ? (float)$scoreRaw : null;
$rowClass = '';
if ($scoreVal !== null) {
$rowClass = $scoreVal < 50 ? 'grade-red' : 'grade-orange';
}
$studentName = trim((string)($row['firstname'] ?? '') . ' ' . (string)($row['lastname'] ?? ''));
$studentLabel = $studentName !== '' ? $studentName : 'N/A';
$currentDecision = (string)($row['decision'] ?? '');
$currentNotes = (string)($row['decision_notes'] ?? '');
$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) ?>">
<td><?= esc($studentName !== '' ? $studentName : 'N/A') ?></td>
<td><?= esc($studentLabel) ?></td>
<td><?= esc($row['class_section_name'] ?? '—') ?></td>
<td class="text-center">
<div class="fw-semibold"><?= $displayScore($scoreRaw) ?></div>
<button type="button"
class="btn btn-outline-secondary btn-xs mt-1 btn-show-details"
style="font-size:0.72rem;padding:1px 7px;"
data-student-id="<?= (int)($row['student_id'] ?? 0) ?>"
data-student-name="<?= esc($studentName !== '' ? $studentName : 'N/A') ?>"
data-student-name="<?= esc($studentLabel) ?>"
data-school-year="<?= esc((string)($schoolYear ?? '')) ?>">
Details
</button>
</td>
<td>
<form method="post" action="<?= site_url('grading/below-60/decisions/save') ?>">
<?= csrf_field() ?>
<input type="hidden" name="student_id" value="<?= esc((string)($row['student_id'] ?? '')) ?>">
<input type="hidden" name="semester" value="<?= esc((string)($semester ?? '')) ?>">
<input type="hidden" name="school_year" value="<?= esc((string)($schoolYear ?? '')) ?>">
<input type="hidden"
name="student_id"
value="<?= esc((string)($row['student_id'] ?? '')) ?>">
<input type="hidden"
name="semester"
value="year">
<input type="hidden"
name="school_year"
value="<?= esc((string)($schoolYear ?? '')) ?>">
<textarea name="notes"
class="form-control form-control-sm decision-notes"
rows="3"
placeholder="Add comments or rationale…"><?= esc($currentNotes) ?></textarea>
<div class="d-flex gap-2 mt-2 align-items-center">
<select name="decision" class="form-select form-select-sm decision-select flex-grow-1">
<select name="decision"
class="form-select form-select-sm decision-select flex-grow-1">
<?php foreach ($decisionOptions as $val => $label): ?>
<option value="<?= esc($val) ?>" <?= $currentDecision === $val ? 'selected' : '' ?>>
<?= esc($label) ?>
</option>
<?php endforeach; ?>
</select>
<button type="submit" class="btn btn-sm btn-primary">Save</button>
<button type="submit" class="btn btn-sm btn-primary">
Save
</button>
</div>
</form>
</td>
<td class="text-center align-middle">
<?php if ($currentDecision !== '' && $badge): ?>
<span class="badge bg-<?= esc($badge) ?> fs-6 px-3 py-2 d-block mb-2"><?= esc($currentDecision) ?></span>
<span class="badge bg-<?= esc($badge) ?> fs-6 px-3 py-2 d-block mb-2">
<?= esc($currentDecision) ?>
</span>
<button type="button"
class="btn btn-sm btn-outline-primary btn-send-email"
data-student-id="<?= (int)($row['student_id'] ?? 0) ?>"
data-semester="<?= esc((string)($semester ?? '')) ?>"
data-semester="year"
data-school-year="<?= esc((string)($schoolYear ?? '')) ?>">
Send Email
</button>
@@ -151,21 +207,21 @@
<?php endif; ?>
</td>
<?php
// Final (consolidated) decision from student_decisions table
$finalDecision = $row['consolidated_decision'] ?? null;
$finalBadge = $finalDecision !== null ? ($decisionBadge[$finalDecision] ?? 'secondary') : null;
?>
<td class="text-center align-middle">
<?php if ($finalDecision !== null && $finalDecision !== ''): ?>
<span class="badge bg-<?= esc($finalBadge) ?> px-2 py-1"><?= esc($finalDecision) ?></span>
<span class="badge bg-<?= esc($finalBadge) ?> px-2 py-1">
<?= esc($finalDecision) ?>
</span>
<?php else: ?>
<a href="<?= site_url('grading/decisions?' . http_build_query(['semester' => $semester ?? '', 'school_year' => $schoolYear ?? ''])) ?>"
class="text-muted small">Generate</a>
<a href="<?= site_url('grading/decisions?' . http_build_query([
'school_year' => $schoolYear ?? '',
])) ?>"
class="text-muted small">
Generate
</a>
<?php endif; ?>
</td>
<?php $certNumber = (string)($row['certificate_number'] ?? ''); ?>
<td class="text-center align-middle">
<?php if ($certNumber !== ''): ?>
<a href="<?= site_url('administrator/certificates/reprint/' . rawurlencode($certNumber)) ?>"
@@ -186,9 +242,14 @@
<div class="mt-3 mb-4 d-flex gap-2 flex-wrap">
<?php foreach ($decisionBadge as $label => $color): ?>
<span class="badge bg-<?= esc($color) ?> px-3 py-2"><?= esc($label) ?></span>
<span class="badge bg-<?= esc($color) ?> px-3 py-2">
<?= esc($label) ?>
</span>
<?php endforeach; ?>
<span class="text-muted small align-self-center ms-1">— decision colour key</span>
<span class="text-muted small align-self-center ms-1">
— decision colour key
</span>
</div>
<?php endif; ?>
</div>
@@ -202,13 +263,19 @@
<h5 class="modal-title" id="detailsModalLabel">Score Details</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body" id="detailsModalBody">
<div class="text-center py-4">
<div class="spinner-border text-primary" role="status"></div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Close</button>
<button type="button"
class="btn btn-outline-secondary"
data-bs-dismiss="modal">
Close
</button>
</div>
</div>
</div>
@@ -232,29 +299,46 @@
<div class="alert alert-danger mb-0" id="emailModalErrorMsg"></div>
</div>
<form id="decisionEmailForm" method="post"
<form id="decisionEmailForm"
method="post"
action="<?= site_url('grading/below-60/decisions/email') ?>"
style="display:none;">
<?= csrf_field() ?>
<input type="hidden" name="student_id" id="emailStudentId">
<input type="hidden" name="semester" id="emailSemester">
<input type="hidden" name="student_id" id="emailStudentId">
<input type="hidden" name="semester" id="emailSemester" value="year">
<input type="hidden" name="school_year" id="emailSchoolYear">
<input type="hidden" name="html" id="emailHtmlHidden">
<input type="hidden" name="html" id="emailHtmlHidden">
<div class="modal-body">
<div class="mb-3">
<label class="form-label fw-semibold" for="emailSubjectInput">Subject</label>
<input type="text" class="form-control" id="emailSubjectInput" name="subject" required>
<input type="text"
class="form-control"
id="emailSubjectInput"
name="subject"
required>
</div>
<div class="mb-1">
<label class="form-label fw-semibold">Body</label>
</div>
<textarea id="decisionEmailEditor" rows="18"></textarea>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary" id="emailSendBtn">Send Email</button>
<button type="button"
class="btn btn-outline-secondary"
data-bs-dismiss="modal">
Cancel
</button>
<button type="submit"
class="btn btn-primary"
id="emailSendBtn">
Send Email
</button>
</div>
</form>
</div>
@@ -265,34 +349,55 @@
<?= $this->section('scripts') ?>
<style>
.grade-orange td { background: #fff3cd !important; color: #8a5d00; }
.grade-red td { background: #f8d7da !important; color: #842029; }
.decision-notes { font-size: 0.85rem; resize: vertical; min-height: 60px; }
.decisions-dt td { vertical-align: top; }
.grade-orange td {
background: #fff3cd !important;
color: #8a5d00;
}
.grade-red td {
background: #f8d7da !important;
color: #842029;
}
.decision-notes {
font-size: 0.85rem;
resize: vertical;
min-height: 60px;
}
.decisions-dt td {
vertical-align: top;
}
</style>
<script src="<?= base_url('assets/tinymce/tinymce.min.js') ?>"></script>
<script>
(function () {
// DataTable
if (window.$ && $.fn && $.fn.DataTable) {
$(function () {
const tbl = $('.decisions-dt');
if (!tbl.length) return;
try {
tbl.DataTable({
order: [[2, 'asc']],
pageLength: 100,
lengthMenu: [10, 25, 50, 100, 200],
columnDefs: [{ orderable: false, targets: [3] }]
columnDefs: [
{ orderable: false, targets: [3] }
]
});
} catch (_) {}
});
}
// ── Details modal ────────────────────────────────────────────
const detailsModal = document.getElementById('detailsModal');
const detailsModalBody = document.getElementById('detailsModalBody');
const detailsModalTitle= document.getElementById('detailsModalLabel');
const detailsModal = document.getElementById('detailsModal');
const detailsModalBody = document.getElementById('detailsModalBody');
const detailsModalTitle = document.getElementById('detailsModalLabel');
const SCORE_LABELS = {
homework_avg: 'Homework Avg',
@@ -303,7 +408,7 @@
attendance_score: 'Attendance',
midterm_exam_score: 'Midterm Score',
final_exam_score: 'Final Exam',
semester_score: 'Semester Score',
semester_score: 'Semester Score'
};
const COMMENT_TYPE_LABELS = {
@@ -312,12 +417,14 @@
attendance_comment: 'Attendance',
midterm: 'Midterm',
final: 'Final Exam',
ptap: 'PTAP',
ptap: 'PTAP'
};
function fmtScore(v) {
if (v === null || v === '' || v === undefined) return '—';
const n = parseFloat(v);
return isNaN(n) ? v : n.toFixed(2);
}
@@ -333,78 +440,110 @@
if (!semesters || semesters.length === 0) {
return '<div class="alert alert-warning mb-0">No score data found.</div>';
}
let html = '';
semesters.forEach(function (sem) {
html += '<h6 class="fw-bold mt-3 mb-2">' + esc(sem.semester || '') + ' Semester';
if (sem.class_section_name) html += ' <span class="text-muted fw-normal fs-6">— ' + esc(sem.class_section_name) + '</span>';
if (sem.class_section_name) {
html += ' <span class="text-muted fw-normal fs-6">— ' + esc(sem.class_section_name) + '</span>';
}
html += '</h6>';
// Scores table
html += '<table class="table table-sm table-bordered mb-2">';
html += '<thead class="table-light"><tr><th>Item</th><th class="text-center">Score</th></tr></thead><tbody>';
let hasRow = false;
Object.entries(SCORE_LABELS).forEach(function ([key, label]) {
const v = sem[key];
if (v === null || v === '' || v === undefined) return;
const bold = key === 'semester_score' ? ' fw-bold' : '';
html += '<tr><td>' + label + '</td><td class="text-center' + bold + '">' + fmtScore(v) + '</td></tr>';
html += '<tr><td>' + esc(label) + '</td><td class="text-center' + bold + '">' + esc(fmtScore(v)) + '</td></tr>';
hasRow = true;
});
if (!hasRow) html += '<tr><td colspan="2" class="text-muted">No scores recorded.</td></tr>';
if (!hasRow) {
html += '<tr><td colspan="2" class="text-muted">No scores recorded.</td></tr>';
}
html += '</tbody></table>';
// Comments section
const comments = sem.comments || {};
const commentEntries = Object.entries(comments).filter(function ([, v]) { return v && v.trim(); });
// Deduplicate attendance + attendance_comment (show once)
const commentEntries = Object.entries(comments).filter(function ([, v]) {
return v && String(v).trim();
});
const seen = {};
const deduped = [];
commentEntries.forEach(function ([type, text]) {
const label = COMMENT_TYPE_LABELS[type] || type;
const key = label + '|' + text.trim();
if (!seen[key]) { seen[key] = true; deduped.push([label, text]); }
const key = label + '|' + String(text).trim();
if (!seen[key]) {
seen[key] = true;
deduped.push([label, String(text)]);
}
});
if (deduped.length > 0) {
html += '<div class="mb-3">';
html += '<p class="fw-semibold mb-1" style="font-size:0.9rem;">Comments</p>';
deduped.forEach(function ([label, text]) {
html += '<div class="mb-2 p-2 bg-light rounded border-start border-3 border-secondary">';
html += '<span class="badge bg-secondary me-1" style="font-size:0.7rem;">' + esc(label) + '</span>';
html += '<span class="text-dark" style="font-size:0.9rem;">' + esc(text).replace(/\n/g, '<br>') + '</span>';
html += '</div>';
});
html += '</div>';
}
});
return html;
}
if (detailsModal) {
document.addEventListener('click', function (e) {
const btn = e.target.closest('.btn-show-details');
if (!btn) return;
const studentId = btn.dataset.studentId;
const studentName= btn.dataset.studentName;
const schoolYear = btn.dataset.schoolYear;
const studentId = btn.dataset.studentId;
const studentName = btn.dataset.studentName;
const schoolYear = btn.dataset.schoolYear;
detailsModalTitle.textContent = studentName + ' — Score Details';
detailsModalBody.innerHTML = '<div class="text-center py-4"><div class="spinner-border text-primary" role="status"></div></div>';
bootstrap.Modal.getOrCreateInstance(detailsModal).show();
const url = '<?= site_url('grading/below-60/decisions/student-details') ?>'
+ '?student_id=' + encodeURIComponent(studentId)
+ '&school_year=' + encodeURIComponent(schoolYear);
fetch(url, { headers: { 'X-Requested-With': 'XMLHttpRequest' } })
.then(function (r) { return r.json(); })
fetch(url, {
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(function (r) {
return r.json();
})
.then(function (data) {
if (data.error) {
detailsModalBody.innerHTML = '<div class="alert alert-danger">' + data.error + '</div>';
detailsModalBody.innerHTML = '<div class="alert alert-danger">' + esc(data.error) + '</div>';
return;
}
detailsModalBody.innerHTML = buildDetailsHtml(data.semesters);
})
.catch(function () {
@@ -429,8 +568,8 @@
function showPane(which) {
loadingPane.style.display = which === 'loading' ? '' : 'none';
errorPane.style.display = which === 'error' ? '' : 'none';
form.style.display = which === 'form' ? '' : 'none';
errorPane.style.display = which === 'error' ? '' : 'none';
form.style.display = which === 'form' ? '' : 'none';
}
function destroyEditor() {
@@ -441,6 +580,7 @@
function initEditor(html) {
if (!window.tinymce) return;
tinymce.init({
selector: '#decisionEmailEditor',
base_url: '<?= base_url('assets/tinymce') ?>',
@@ -462,65 +602,76 @@
setup(editor) {
editor.on('init', function () {
editor.setContent(html);
if (htmlHidden) htmlHidden.value = html;
if (htmlHidden) {
htmlHidden.value = html;
}
});
editor.on('keyup change undo redo SetContent', function () {
if (htmlHidden) htmlHidden.value = editor.getContent({ format: 'html' });
if (htmlHidden) {
htmlHidden.value = editor.getContent({ format: 'html' });
}
});
}
});
}
// Sync hidden field before form submit
form.addEventListener('submit', function () {
if (window.tinymce) {
const ed = tinymce.get('decisionEmailEditor');
if (ed && htmlHidden) htmlHidden.value = ed.getContent({ format: 'html' });
if (ed && htmlHidden) {
htmlHidden.value = ed.getContent({ format: 'html' });
}
}
});
// Destroy editor when modal closes
modal.addEventListener('hidden.bs.modal', function () {
destroyEditor();
showPane('loading');
});
// Open modal when "Send Email" is clicked
document.addEventListener('click', function (e) {
const btn = e.target.closest('.btn-send-email');
if (!btn) return;
const studentId = btn.dataset.studentId;
const semester = btn.dataset.semester;
const schoolYear = btn.dataset.schoolYear;
studentIdIn.value = studentId;
semesterIn.value = semester;
semesterIn.value = 'year';
schoolYearIn.value = schoolYear;
showPane('loading');
// Open the modal immediately (shows spinner)
const bsModal = bootstrap.Modal.getOrCreateInstance(modal);
bsModal.show();
// Fetch the pre-rendered email
const url = '<?= site_url('grading/below-60/decisions/email/preview') ?>'
+ '?student_id=' + encodeURIComponent(studentId)
+ '&semester=' + encodeURIComponent(semester)
+ '&school_year='+ encodeURIComponent(schoolYear);
+ '&semester=year'
+ '&school_year=' + encodeURIComponent(schoolYear);
fetch(url, { headers: { 'X-Requested-With': 'XMLHttpRequest' } })
.then(function (res) { return res.json(); })
fetch(url, {
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(function (res) {
return res.json();
})
.then(function (data) {
if (data.error) {
errorMsg.textContent = data.error;
showPane('error');
return;
}
subjectInput.value = data.subject || '';
// Reset textarea content before init
document.getElementById('decisionEmailEditor').value = data.html || '';
showPane('form');
initEditor(data.html || '');
})
@@ -531,4 +682,4 @@
});
})();
</script>
<?= $this->endSection() ?>
<?= $this->endSection() ?>