584 lines
32 KiB
PHP
Executable File
584 lines
32 KiB
PHP
Executable File
<?= $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 class’s 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: don’t 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() ?>
|