recreate project

This commit is contained in:
root
2026-02-10 22:11:06 -05:00
commit 663c0cdbda
10149 changed files with 1379710 additions and 0 deletions
+583
View File
@@ -0,0 +1,583 @@
<?= $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() ?>