Files
2026-06-01 02:08:27 -04:00

702 lines
28 KiB
PHP

<?= $this->extend('layout/management_layout') ?>
<?= $this->section('content') ?>
<?php
// This page is Whole Year only.
// Do not expose semester filter here.
$semester = 'year';
$schoolYear = $schoolYear ?? '';
$schoolYears = $schoolYears ?? [];
if (empty($schoolYears) && $schoolYear !== '') {
$schoolYears = [$schoolYear];
}
?>
<div class="container-fluid">
<div class="wrapper below-sixty-decisions-wrapper">
<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="text-muted">
Whole Year • <?= esc($schoolYear) ?>
</div>
<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' => '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([
'school_year' => $schoolYear,
])) ?>">
All Decisions
</a>
<?php if (!empty($canViewGrading)): ?>
<a class="btn btn-outline-secondary btn-sm" href="<?= base_url('grading') ?>">
Grading
</a>
<?php endif; ?>
</div>
</div>
<?php if (!empty(session()->getFlashdata('status'))): ?>
<div class="alert alert-success alert-dismissible fade show" role="alert">
<?= esc(session()->getFlashdata('status')) ?>
<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')) ?>
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
<?php endif; ?>
<?php
$decisionOptions = [
'' => '— No decision yet —',
'Pass' => 'Pass',
'Repeat Class' => 'Repeat Class',
'Make-up exam in fall' => 'Make-up exam in fall',
'Deferred decision' => 'Deferred decision',
'Expel' => 'Expel',
'Withdrawn' => 'Withdrawn',
];
$decisionBadge = [
'Pass' => 'success',
'Repeat Class' => 'danger',
'Make-up exam in fall' => '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, '.', ''));
}
return esc($value);
};
?>
<?php if (empty($rows)): ?>
<div class="alert alert-success text-center d-inline-block">
No students below 60 for the whole year in <?= esc($schoolYear) ?>.
</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>
<thead class="table-light">
<tr>
<th style="min-width:160px">Student Name</th>
<th class="text-center">Age</th>
<th>Class-Section</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>
</tr>
</thead>
<tbody>
<?php foreach ($rows as $row): ?>
<?php
// Whole-year page: use year_score only.
// Do not fall back to semester_score, because this page should not display semester results.
$scoreRaw = $row['year_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;
?>
<tr class="<?= esc($rowClass) ?>">
<td><?= esc($studentLabel) ?></td>
<td class="text-center"><?= esc((string)($row['age'] ?? '—')) ?></td>
<td><?= esc($row['class_section_name'] ?? '—') ?></td>
<td class="text-center" data-order="<?= esc($scoreVal !== null ? (string)$scoreVal : '-1') ?>">
<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($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="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">
<?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>
</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>
<button type="button"
class="btn btn-sm btn-outline-primary btn-send-email"
data-student-id="<?= (int)($row['student_id'] ?? 0) ?>"
data-semester="year"
data-school-year="<?= esc((string)$schoolYear) ?>">
Send Email
</button>
<?php else: ?>
<span class="text-muted small">Pending</span>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<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>
<?php endforeach; ?>
<span class="text-muted small align-self-center ms-1">
— decision colour key
</span>
</div>
<?php endif; ?>
</div>
</div>
<!-- Score details modal -->
<div class="modal fade" id="detailsModal" tabindex="-1" aria-labelledby="detailsModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header">
<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>
</div>
</div>
</div>
</div>
<!-- Email editor modal -->
<div class="modal fade" id="decisionEmailModal" tabindex="-1" aria-labelledby="decisionEmailModalLabel" aria-hidden="true">
<div class="modal-dialog modal-xl modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="decisionEmailModalLabel">Send Decision Email</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div id="emailModalLoading" class="modal-body text-center py-5" style="display:none;">
<div class="spinner-border text-primary" role="status"></div>
<div class="mt-2 text-muted">Loading email template…</div>
</div>
<div id="emailModalError" class="modal-body" style="display:none;">
<div class="alert alert-danger mb-0" id="emailModalErrorMsg"></div>
</div>
<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" value="year">
<input type="hidden" name="school_year" id="emailSchoolYear">
<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>
</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>
</div>
</form>
</div>
</div>
</div>
<?= $this->endSection() ?>
<?= $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;
}
</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: [4] }
]
});
} catch (_) {}
});
}
// ── Details modal ────────────────────────────────────────────
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(v) {
if (v === null || v === '' || v === undefined) return '—';
const n = parseFloat(v);
return isNaN(n) ? v : n.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>';
}
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 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>' + 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>';
}
html += '</tbody></table>';
const comments = sem.comments || {};
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 + '|' + 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;
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();
})
.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>';
});
});
}
// ── Email modal ───────────────────────────────────────────────
const modal = document.getElementById('decisionEmailModal');
const loadingPane = document.getElementById('emailModalLoading');
const errorPane = document.getElementById('emailModalError');
const errorMsg = document.getElementById('emailModalErrorMsg');
const form = document.getElementById('decisionEmailForm');
const subjectInput = document.getElementById('emailSubjectInput');
const htmlHidden = document.getElementById('emailHtmlHidden');
const studentIdIn = document.getElementById('emailStudentId');
const semesterIn = document.getElementById('emailSemester');
const schoolYearIn = document.getElementById('emailSchoolYear');
if (!modal) return;
function showPane(which) {
loadingPane.style.display = which === 'loading' ? '' : 'none';
errorPane.style.display = which === 'error' ? '' : 'none';
form.style.display = which === 'form' ? '' : 'none';
}
function destroyEditor() {
if (window.tinymce) {
tinymce.remove('#decisionEmailEditor');
}
}
function initEditor(html) {
if (!window.tinymce) return;
tinymce.init({
selector: '#decisionEmailEditor',
base_url: '<?= base_url('assets/tinymce') ?>',
suffix: '.min',
license_key: 'gpl',
height: 460,
menubar: true,
branding: false,
promotion: false,
plugins: 'advlist autolink lists link image charmap preview anchor ' +
'searchreplace visualblocks code fullscreen insertdatetime media table ' +
'help wordcount emoticons codesample',
toolbar: 'undo redo | blocks fontfamily fontsize | bold italic underline strikethrough forecolor backcolor | ' +
'alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | ' +
'link image media table | emoticons codesample | removeformat | preview code',
convert_urls: false,
paste_data_images: true,
content_style: 'body { font-family: system-ui, -apple-system, Segoe UI, Roboto, Helvetica Neue, Arial, sans-serif; font-size: 14px; }',
setup(editor) {
editor.on('init', function () {
editor.setContent(html);
if (htmlHidden) {
htmlHidden.value = html;
}
});
editor.on('keyup change undo redo SetContent', function () {
if (htmlHidden) {
htmlHidden.value = editor.getContent({ format: 'html' });
}
});
}
});
}
form.addEventListener('submit', function () {
if (window.tinymce) {
const ed = tinymce.get('decisionEmailEditor');
if (ed && htmlHidden) {
htmlHidden.value = ed.getContent({ format: 'html' });
}
}
});
modal.addEventListener('hidden.bs.modal', function () {
destroyEditor();
showPane('loading');
});
document.addEventListener('click', function (e) {
const btn = e.target.closest('.btn-send-email');
if (!btn) return;
const studentId = btn.dataset.studentId;
const schoolYear = btn.dataset.schoolYear;
studentIdIn.value = studentId;
semesterIn.value = 'year';
schoolYearIn.value = schoolYear;
showPane('loading');
const bsModal = bootstrap.Modal.getOrCreateInstance(modal);
bsModal.show();
const url = '<?= site_url('grading/below-60/decisions/email/preview') ?>'
+ '?student_id=' + encodeURIComponent(studentId)
+ '&semester=year'
+ '&school_year=' + encodeURIComponent(schoolYear);
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 || '';
document.getElementById('decisionEmailEditor').value = data.html || '';
showPane('form');
initEditor(data.html || '');
})
.catch(function () {
errorMsg.textContent = 'Failed to load email template. Please try again.';
showPane('error');
});
});
})();
</script>
<?= $this->endSection() ?>