Files
2026-02-10 22:11:06 -05:00

584 lines
32 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?= $this->extend('layout/management_layout') ?>
<?= $this->section('content') ?>
<div class="container-fluid">
<div class="wrapper">
<div class="content mb-3"></div>
<h2 class="text-center mt-4 mb-3">Grading Management</h2>
<?php
// Expect: $grades is keyed by class_id (1..11, 12=Youth, 13=KG)
// Build stable order: KG (13) -> Grades 1..11 -> Youth (12) -> any other ids (ascending)
$semester = $semester ?? (session()->get('semester') ?? 'Fall');
$schoolYear = $schoolYear ?? (session()->get('school_year') ?? date('Y') . '-' . (date('Y') + 1));
$requestedClassId = isset($requestedClassId) ? (int)$requestedClassId : 0;
$semesterOptions = $semesterOptions ?? [];
$scoreLocks = $scoreLocks ?? [];
if (empty($semesterOptions)) {
$semesterOptions = ['Fall', 'Spring'];
}
if ($semester !== '' && !in_array($semester, $semesterOptions, true)) {
$semesterOptions[] = $semester;
}
$semesterOptions = array_values(array_unique($semesterOptions));
// Slug from class_id
$slugFor = function (int $cid): string {
if ($cid === 13) return 'kg';
if ($cid === 12) return 'youth';
if ($cid >= 1 && $cid <= 11) return 'g' . $cid;
return (string)$cid;
};
// Compose ordered class ids from keys actually present in $grades
$presentIds = array_map('intval', array_keys($grades));
$presentSet = array_flip($presentIds);
$sortedClassIds = [];
if (isset($presentSet[13])) $sortedClassIds[] = 13; // KG first
for ($g = 1; $g <= 11; $g++) if (isset($presentSet[$g])) $sortedClassIds[] = $g; // Grades 1..11
if (isset($presentSet[12])) $sortedClassIds[] = 12; // Youth last
// Include any other unexpected ids (edge cases) without breaking
$others = array_diff($presentIds, $sortedClassIds);
sort($others, SORT_NUMERIC);
$sortedClassIds = array_merge($sortedClassIds, $others);
// Label overrides for non-standard class ids (e.g., Arabic-1)
$classLabelById = [];
foreach ($grades as $cid => $sections) {
if (!empty($sections[0]['class_section_name'])) {
$classLabelById[(int) $cid] = (string) $sections[0]['class_section_name'];
}
}
// Label from class_id
$labelFor = function (int $cid) use ($classLabelById): string {
if ($cid === 13) return 'KG';
if ($cid === 12) return 'Youth';
if ($cid >= 1 && $cid <= 11) return 'Grade ' . $cid;
if (!empty($classLabelById[$cid])) return $classLabelById[$cid];
return 'Class ' . $cid;
};
// Distinct student counts per class_id for tab badges
$studentsCountByClassId = [];
foreach ($grades as $cid => $sections) {
$uniq = [];
foreach ($sections as $section) {
$secId = (int)($section['class_section_id'] ?? 0);
if ($secId > 0 && !empty($studentsBySection[$secId])) {
foreach ($studentsBySection[$secId] as $stu) {
if (!empty($stu['id'])) $uniq[(int)$stu['id']] = true;
}
}
}
$studentsCountByClassId[(int)$cid] = count($uniq);
}
$scoreStatusByClass = [];
foreach ($grades as $cid => $sections) {
$allLocked = true;
$hasSection = false;
foreach ($sections as $section) {
$secId = (int)($section['class_section_id'] ?? 0);
if ($secId <= 0) {
continue;
}
$hasSection = true;
if (empty($scoreLocks[$secId])) {
$allLocked = false;
break;
}
}
$scoreStatusByClass[(int)$cid] = $hasSection && $allLocked;
}
// Default active tab
$defaultId = ($requestedClassId > 0 && isset($presentSet[$requestedClassId]))
? $requestedClassId
: ($sortedClassIds[0] ?? null);
// Sort each classs sections by displayed name (e.g., 1-A, 1-B…)
foreach ($grades as $cid => &$sections) {
usort($sections, function ($a, $b) {
return strnatcasecmp($a['class_section_name'] ?? '', $b['class_section_name'] ?? '');
});
}
unset($sections);
?>
<div class="d-flex justify-content-between align-items-center mt-2 mb-2 flex-wrap gap-2">
<div class="text-muted">
<?= esc(ucfirst($semester)) ?> • <?= esc($schoolYear) ?>
</div>
<div class="d-flex align-items-center gap-2 flex-wrap">
<?php
$scoresReleasedFall = $scoresReleasedFall ?? false;
$scoresReleasedSpring = $scoresReleasedSpring ?? false;
?>
<form action="<?= base_url('grading/release-scores') ?>" method="post" class="d-flex align-items-center gap-2">
<?= csrf_field() ?>
<input type="hidden" name="semester" value="Fall">
<button type="submit" class="btn btn-sm <?= $scoresReleasedFall ? 'btn-outline-danger' : 'btn-outline-success' ?>">
<?= $scoresReleasedFall ? 'Release scores for Parent - Fall: ON' : 'Release scores for Parent - Fall: OFF' ?>
</button>
</form>
<form action="<?= base_url('grading/release-scores') ?>" method="post" class="d-flex align-items-center gap-2">
<?= csrf_field() ?>
<input type="hidden" name="semester" value="Spring">
<button type="submit" class="btn btn-sm <?= $scoresReleasedSpring ? 'btn-outline-danger' : 'btn-outline-success' ?>">
<?= $scoresReleasedSpring ? 'Release scores for Parent - Spring: ON' : 'Release scores for Parent - Spring: OFF' ?>
</button>
</form>
<form action="<?= base_url('grading/lock-all-scores') ?>" method="post" class="d-flex align-items-center gap-2">
<?= csrf_field() ?>
<input type="hidden" name="semester" value="<?= esc($semester) ?>">
<input type="hidden" name="school_year" value="<?= esc($schoolYear) ?>">
<button type="submit" class="btn btn-sm btn-outline-danger">
Lock All Scores
</button>
</form>
<a class="btn btn-outline-primary btn-sm"
href="<?= base_url('grading/below-60?semester=' . rawurlencode($semester) . '&school_year=' . rawurlencode($schoolYear)) ?>">
Below 60 Summary
</a>
<form id="semesterFilterForm" class="d-flex align-items-center gap-2" method="get">
<label for="gradingSemesterSelect" class="mb-0 text-secondary small">Semester</label>
<select id="gradingSemesterSelect" name="semester" class="form-select form-select-sm">
<?php foreach ($semesterOptions as $option): ?>
<option value="<?= esc($option) ?>" <?= strcasecmp($option, $semester) === 0 ? 'selected' : '' ?>>
<?= esc($option) ?>
</option>
<?php endforeach; ?>
</select>
<?php if ($requestedClassId > 0): ?>
<input type="hidden" name="class_id" value="<?= esc($requestedClassId) ?>">
<?php endif; ?>
</form>
</div>
</div>
<?php if (session()->get('status')): ?>
<div class="alert alert-success">
<?= session()->get('status') ?>
</div>
<?php endif; ?>
<?php if (session()->get('error')): ?>
<div class="alert alert-danger">
<?= session()->get('error') ?>
</div>
<?php endif; ?>
<?php if (empty($sortedClassIds)): ?>
<div class="alert alert-warning text-center d-inline-block">
No class data available for the current semester.
</div>
<?php else: ?>
<!-- Tabs navigation -->
<ul class="nav nav-tabs justify-content-center" id="gradingTabs" role="tablist">
<?php foreach ($sortedClassIds as $cid): ?>
<?php
$slug = $slugFor($cid);
$label = $labelFor($cid);
$cnt = (int)($studentsCountByClassId[$cid] ?? 0);
$statusDone = !empty($scoreStatusByClass[$cid]);
$statusTitle = $statusDone
? 'Scores submitted for all sections'
: 'Scores pending for one or more sections';
?>
<li class="nav-item">
<a class="nav-link <?= ($cid === $defaultId) ? 'active' : '' ?>"
id="grade<?= esc($slug) ?>-tab"
data-bs-toggle="tab"
href="#grade<?= esc($slug) ?>"
role="tab"
aria-controls="grade<?= esc($slug) ?>"
aria-selected="<?= ($cid === $defaultId) ? 'true' : 'false' ?>"
data-class-id="<?= (int)$cid ?>">
<span class="score-status-light <?= $statusDone ? 'done' : 'pending' ?>"
title="<?= esc($statusTitle) ?>"
aria-hidden="true"></span>
<span class="visually-hidden"><?= esc($statusTitle) ?></span>
<?= esc($label) ?>
<span class="badge bg-secondary ms-1" title="Students in <?= esc($label) ?>">
<?= $cnt ?>
</span>
</a>
</li>
<?php endforeach; ?>
</ul>
<!-- Tabs content -->
<div class="tab-content mt-3">
<?php foreach ($sortedClassIds as $cid): ?>
<?php
$slug = $slugFor($cid);
$label = $labelFor($cid);
$sections = $grades[$cid] ?? [];
?>
<div class="tab-pane fade <?= ($cid === $defaultId) ? 'show active' : '' ?>"
id="grade<?= esc($slug) ?>"
role="tabpanel"
aria-labelledby="grade<?= esc($slug) ?>-tab">
<h3 class="text-center"><?= esc($label) ?> — Class Sections</h3>
<?php if (empty($sections)): ?>
<div class="alert alert-light border mt-3 text-center">No sections in this class.</div>
<?php else: ?>
<?php
// Safely extract a scalar from possibly nested/array values
$scalar = function ($value, array $tryKeys = []) {
if (is_null($value)) return null;
if (is_scalar($value)) return $value;
if (is_array($value)) {
// Try a few common keys by priority, then the first numeric value
foreach ($tryKeys as $k) {
if (array_key_exists($k, $value) && is_scalar($value[$k])) {
return $value[$k];
}
}
foreach ($value as $v) {
if (is_scalar($v)) return $v;
}
}
// Fallback: dont break rendering
return null;
};
$displayScore = function ($value) {
if ($value === null || $value === '' || (is_string($value) && strtolower(trim($value)) === 'null')) {
return '-';
}
return esc($value);
};
?>
<?php
$gradingQuery = 'semester=' . rawurlencode($semester) . '&school_year=' . rawurlencode($schoolYear);
foreach ($sections as $section):
?>
<?php
$sectionId = (int)($section['class_section_id'] ?? 0);
$sectionName = (string)($section['class_section_name'] ?? '');
$students = $sectionId ? ($studentsBySection[$sectionId] ?? []) : [];
$locked = !empty($scoreLocks[$sectionId]);
?>
<?php if ($sectionId <= 0): continue;
endif; ?>
<div class="card mb-4 shadow-sm">
<div class="card-header d-flex justify-content-between align-items-center">
<div>
<strong>Section:</strong> <?= esc($sectionName) ?>
<span class="badge bg-info ms-2"><?= count($students) ?> students</span>
<?php if ($locked): ?>
<span class="badge bg-success ms-2">Scores submitted</span>
<?php else: ?>
<span class="badge bg-danger ms-2">Scores Not submitted</span>
<?php endif; ?>
</div>
<div class="d-flex flex-wrap gap-2">
<!-- Per-section toolbar -->
<form action="<?= base_url('grading/toggle-score-lock') ?>" method="post" class="d-inline">
<?= csrf_field() ?>
<input type="hidden" name="class_section_id" value="<?= esc($sectionId) ?>">
<input type="hidden" name="semester" value="<?= esc($semester) ?>">
<input type="hidden" name="school_year" value="<?= esc($schoolYear) ?>">
<button type="submit" class="btn btn-sm <?= $locked ? 'btn-outline-danger' : 'btn-outline-success' ?>">
<?= $locked ? 'Unlock Scores' : 'Lock Scores' ?>
</button>
</form>
<a href="<?= base_url('grading/homework?class_section_id=' . $sectionId . '&' . $gradingQuery) ?>"
class="btn btn-secondary btn-sm">Homework</a>
<a href="<?= base_url('grading/quiz?class_section_id=' . $sectionId . '&' . $gradingQuery) ?>"
class="btn btn-secondary btn-sm">Quiz</a>
<a href="<?= base_url('grading/project?class_section_id=' . $sectionId . '&' . $gradingQuery) ?>"
class="btn btn-secondary btn-sm">Project</a>
<a href="<?= base_url('grading/participation?class_section_id=' . $sectionId . '&' . $gradingQuery) ?>"
class="btn btn-secondary btn-sm">Participation</a>
<?php if (strtolower($semester) === 'fall'): ?>
<a href="<?= base_url('grading/midterm?class_section_id=' . $sectionId . '&' . $gradingQuery) ?>"
class="btn btn-secondary btn-sm">Midterm</a>
<?php elseif (strtolower($semester) === 'spring'): ?>
<a href="<?= base_url('grading/final?class_section_id=' . $sectionId . '&' . $gradingQuery) ?>"
class="btn btn-secondary btn-sm">Final</a>
<?php endif; ?>
<a href="<?= base_url('grading/comments?class_section_id=' . $sectionId . '&' . $gradingQuery) ?>"
class="btn btn-secondary btn-sm">Comments</a>
<form action="<?= base_url('grading/refresh-semester-scores') ?>" method="post" class="d-inline">
<?= csrf_field() ?>
<input type="hidden" name="class_section_id" value="<?= esc($sectionId) ?>">
<input type="hidden" name="semester" value="<?= esc($semester) ?>">
<input type="hidden" name="school_year" value="<?= esc($schoolYear) ?>">
<button type="submit" class="btn btn-outline-secondary btn-sm">Refresh Scores</button>
</form>
</div>
</div>
<div class="card-body">
<?php if (empty($students)): ?>
<div class="alert alert-light border text-center mb-0">No students in this section.</div>
<?php else: ?>
<div class="table-responsive">
<table class="table table-bordered table-striped w-100 grading-table">
<thead class="table-light">
<tr>
<th>School ID</th>
<th>Student First Name</th>
<th>Student Last Name</th>
<th>Homework Avg</th>
<th>Project Avg</th>
<th>Participation</th>
<th>Test/Quiz Avg</th>
<th>Attendance</th>
<th>PTAP</th>
<?php if (strtolower($semester) === 'fall'): ?>
<th>Midterm</th>
<?php elseif (strtolower($semester) === 'spring'): ?>
<th>Final</th>
<?php endif; ?>
<?php if (!empty($_GET['debug'])): ?>
<th>DBG Biz CSID</th>
<th>DBG PK CSID</th>
<?php endif; ?>
<th>Semester Score</th>
</tr>
</thead>
<tbody>
<?php foreach ($students as $student): ?>
<tr>
<td><?= esc($student['school_id'] ?? 'N/A') ?></td>
<td>
<?php $sid = (int)($student['id'] ?? 0); ?>
<?php if ($sid): ?>
<a href="#" class="text-decoration-none" data-family-student-id="<?= $sid ?>">
<?= esc($student['firstname'] ?? 'N/A') ?>
</a>
<?php else: ?>
<?= esc($student['firstname'] ?? 'N/A') ?>
<?php endif; ?>
</td>
<td>
<?php if (!empty($sid)): ?>
<a href="#" class="text-decoration-none" data-family-student-id="<?= $sid ?>">
<?= esc($student['lastname'] ?? 'N/A') ?>
</a>
<?php else: ?>
<?= esc($student['lastname'] ?? 'N/A') ?>
<?php endif; ?>
</td>
<td class="text-center"><?= $displayScore($student['homework_avg'] ?? null) ?></td>
<td class="text-center"><?= $displayScore($student['project_avg'] ?? null) ?></td>
<td class="text-center"><?= $displayScore($student['participation'] ?? null) ?></td>
<td class="text-center"><?= $displayScore($student['quiz_avg'] ?? null) ?></td>
<td class="text-center"><?= $displayScore($student['attendance'] ?? null) ?></td>
<td class="text-center">
<?php
$ptapRaw = $student['ptap_score'] ?? $student['ptap'] ?? null;
$ptapVal = $scalar($ptapRaw, ['ptap_score', 'ptap', 'score', 'value']);
echo $displayScore($ptapVal);
?>
</td>
<?php if (strtolower($semester) === 'fall'): ?>
<td class="text-center"><?= $displayScore($student['midterm_exam'] ?? null) ?></td>
<?php elseif (strtolower($semester) === 'spring'): ?>
<td class="text-center"><?= $displayScore($student['final_exam'] ?? null) ?></td>
<?php endif; ?>
<?php if (!empty($_GET['debug'])): ?>
<td class="text-center"><?= esc($student['matched_biz_csid'] ?? '') ?></td>
<td class="text-center"><?= esc($student['matched_pk_csid'] ?? '') ?></td>
<?php endif; ?>
<td class="text-center">
<?php
$semRaw = $student['semester_score'] ?? null;
$semVal = $scalar($semRaw, ['semester_score', 'final', 'score', 'value']);
echo $displayScore($semVal);
?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
</div>
<?php endforeach; ?>
<?php endif; ?>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
</div>
<?= $this->endSection() ?>
<?= $this->section('scripts') ?>
<style>
.nav-tabs .nav-link.active { font-weight: 600; }
.score-status-light {
width: 12px;
height: 12px;
margin-right: .35rem;
border-radius: 50%;
border: 1px solid rgba(0, 0, 0, .25);
display: inline-block;
vertical-align: text-bottom;
}
.score-status-light.done {
background-color: #198754;
box-shadow: inset 0 0 0 1px rgba(25, 135, 84, .35);
}
.score-status-light.pending {
background-color: #dc3545;
box-shadow: inset 0 0 0 1px rgba(220, 53, 69, .35);
}
</style>
<script>
(function() {
function getSelectedClassIdFromURL() {
const p = new URLSearchParams(window.location.search);
return p.get('class_id');
}
function setSelectedClassIdInURL(classId, replace = true) {
const url = new URL(window.location.href);
if (classId) url.searchParams.set('class_id', classId);
else url.searchParams.delete('class_id');
if (replace) history.replaceState(null, '', url.toString());
else history.pushState(null, '', url.toString());
}
function getSavedClassId() {
try {
return localStorage.getItem('grading.selectedClassId');
} catch {
return null;
}
}
function saveClassId(classId) {
try {
localStorage.setItem('grading.selectedClassId', classId ?? '');
} catch {}
}
function activateByClassId(tabsEl, classId) {
if (!tabsEl) return;
let target = null;
if (classId) {
target = tabsEl.querySelector(`a[data-class-id="${classId}"]`);
// also try numeric vs string mismatch
if (!target) target = tabsEl.querySelector(`a[data-class-id="${parseInt(classId,10)}"]`);
}
if (!target) {
// fallback: keep current active or first tab
target = tabsEl.querySelector('a.nav-link.active') || tabsEl.querySelector('a.nav-link');
}
if (target) new bootstrap.Tab(target).show();
}
document.addEventListener('DOMContentLoaded', function() {
const tabsEl = document.getElementById('gradingTabs');
if (!tabsEl) return;
// 1) Decide which tab to show: URL -> localStorage -> server default
const urlClassId = getSelectedClassIdFromURL();
const lsClassId = getSavedClassId();
const initial = urlClassId || lsClassId || null;
activateByClassId(tabsEl, initial);
// 2) When a tab is shown, persist selection and update URL (no history spam)
tabsEl.addEventListener('shown.bs.tab', function(evt) {
const link = evt.target; // newly activated <a>
const classId = link.getAttribute('data-class-id');
saveClassId(classId);
setSelectedClassIdInURL(classId, /*replace*/ true);
// Adjust any visible DataTables after the tab becomes visible
if (window.$ && $.fn && $.fn.dataTable) {
$.fn.dataTable.tables({
visible: true,
api: true
})
.columns.adjust().draw(false);
}
});
// 3) Handle back/forward navigation
window.addEventListener('popstate', function() {
// Re-parse URL on history changes
const id = getSelectedClassIdFromURL() || getSavedClassId();
activateByClassId(tabsEl, id);
});
});
})();
</script>
<script>
(function(){
function getFixedHeaderOffset() {
let total = 0;
const stack = [];
const header = document.querySelector('header.navbar.sticky-top, header.navbar.fixed-top');
if (header) stack.push(header);
const mgmt = document.getElementById('navbarManagement');
if (mgmt && (mgmt.classList.contains('sticky-top') || mgmt.classList.contains('fixed-top'))) stack.push(mgmt);
document.querySelectorAll('.navbar.sticky-top, .navbar.fixed-top').forEach(el => { if (!stack.includes(el)) stack.push(el); });
stack.forEach(el => { const h = el.offsetHeight || el.getBoundingClientRect().height || 0; total += Math.max(0, Math.round(h)); });
return total;
}
// Initialize DataTables for all grading tables with FixedHeader
$(function() {
if (!$.fn || !$.fn.DataTable) return;
$('.grading-table').each(function(){
try {
const dt = $(this).DataTable({
stateSave: true,
pageLength: 100,
lengthMenu: [10, 25, 50, 100],
order: [[1, 'asc']],
fixedHeader: { header: true, headerOffset: getFixedHeaderOffset() }
});
// If FixedHeader plugin was not available globally, CSS sticky still applies via layout
} catch (_) {}
});
// Ensure columns adjust when a tab is shown (if DOM changed after init)
$('a[data-bs-toggle="tab"]').on('shown.bs.tab', function() {
if ($.fn && $.fn.dataTable) {
$.fn.dataTable.tables({ visible: true, api: true }).columns.adjust().draw(false);
}
});
});
})();
</script>
<script>
document.addEventListener('DOMContentLoaded', function() {
const form = document.getElementById('semesterFilterForm');
const select = document.getElementById('gradingSemesterSelect');
if (!form || !select) return;
select.addEventListener('change', function() {
form.submit();
});
});
</script>
<?= $this->endSection() ?>