recreate project
This commit is contained in:
@@ -0,0 +1,154 @@
|
||||
<?= $this->extend('layout/management_layout') ?>
|
||||
<?= $this->section('content') ?>
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="wrapper below-sixty-wrapper">
|
||||
<h2 class="text-center mt-4 mb-4 below-sixty-title">Below 60 Summary</h2>
|
||||
|
||||
<?= $this->include('partials/academic_filter') ?>
|
||||
|
||||
<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 ?? '') ?>
|
||||
</div>
|
||||
<a class="btn btn-outline-secondary btn-sm" href="<?= base_url('grading') ?>">
|
||||
Back to Grading
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
$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 this selection.
|
||||
</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>
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Student Name</th>
|
||||
<th>Section</th>
|
||||
<th>Hwk Avg</th>
|
||||
<th>Project Avg</th>
|
||||
<th>Participation</th>
|
||||
<th>Test Avg</th>
|
||||
<th>PTAP Score</th>
|
||||
<th>Attendance</th>
|
||||
<th>Midterm Score</th>
|
||||
<th><?= strcasecmp($semester ?? '', 'fall') === 0 ? '1st Semester Score' : 'Semester Score' ?></th>
|
||||
<th>Status</th>
|
||||
<th>Email Parent</th>
|
||||
<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';
|
||||
} elseif ($scoreVal < 60) {
|
||||
$scoreClass = 'grade-orange';
|
||||
}
|
||||
}
|
||||
$studentName = trim((string)($row['firstname'] ?? '') . ' ' . (string)($row['lastname'] ?? ''));
|
||||
?>
|
||||
<?php $isClosed = ($row['status'] ?? 'Open') === 'Closed'; ?>
|
||||
<tr class="<?= esc($scoreClass) ?>">
|
||||
<td><?= esc($studentName !== '' ? $studentName : 'N/A') ?></td>
|
||||
<td><?= esc($row['class_section_name'] ?? '—') ?></td>
|
||||
<td class="text-center"><?= $displayScore($row['homework_avg'] ?? null) ?></td>
|
||||
<td class="text-center"><?= $displayScore($row['project_avg'] ?? null) ?></td>
|
||||
<td class="text-center"><?= $displayScore($row['participation_score'] ?? null) ?></td>
|
||||
<td class="text-center"><?= $displayScore($row['test_avg'] ?? null) ?></td>
|
||||
<td class="text-center"><?= $displayScore($row['ptap_score'] ?? null) ?></td>
|
||||
<td class="text-center"><?= $displayScore($row['attendance_score'] ?? null) ?></td>
|
||||
<td class="text-center"><?= $displayScore($row['midterm_exam_score'] ?? null) ?></td>
|
||||
<td class="text-center"><?= $displayScore($row['semester_score'] ?? null) ?></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">
|
||||
<?= 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 ?? '')) ?>">
|
||||
<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)">
|
||||
<button type="submit" class="btn btn-sm btn-outline-secondary">Update</button>
|
||||
</form>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<form method="post" action="<?= site_url('grading/below-60/email') ?>" class="d-inline">
|
||||
<?= 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 ?? '')) ?>">
|
||||
<button type="submit"
|
||||
class="btn btn-sm <?= $isClosed ? 'btn-secondary' : 'btn-outline-primary' ?>"
|
||||
<?= $isClosed ? 'disabled' : '' ?>>
|
||||
Send Email
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<?php if ($isClosed): ?>
|
||||
<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((string)($semester ?? '')) . '&school_year=' . rawurlencode((string)($schoolYear ?? ''))) ?>">
|
||||
Schedule
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
<?= $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; }
|
||||
</style>
|
||||
<script>
|
||||
(function() {
|
||||
if (!window.$ || !$.fn || !$.fn.DataTable) return;
|
||||
$(function() {
|
||||
const table = $('.below-sixty-dt');
|
||||
if (!table.length) return;
|
||||
try {
|
||||
table.DataTable({
|
||||
order: [[8, 'asc']],
|
||||
pageLength: 100,
|
||||
lengthMenu: [10, 25, 50, 100, 200]
|
||||
});
|
||||
} catch (_) {}
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
<?= $this->endSection() ?>
|
||||
@@ -0,0 +1,491 @@
|
||||
<?= $this->extend('layout/management_layout') ?>
|
||||
<?= $this->section('content') ?>
|
||||
<?php
|
||||
$scoresLocked = !empty($scoresLocked);
|
||||
$lockAttr = $scoresLocked ? 'disabled' : '';
|
||||
?>
|
||||
<div class="container-fluid py-4 px-3">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="text-center mx-auto mb-4" style="max-width: 600px;">
|
||||
<h2 class="text-dark">Edit Student Comments & Reviews (<?= esc($semester) ?> Semester)</h2>
|
||||
</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"><?= esc(session()->get('error')) ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if ($errors = session()->get('errors')): ?>
|
||||
<div class="alert alert-danger">
|
||||
<ul class="mb-0">
|
||||
<?php foreach ($errors as $error): ?>
|
||||
<li><?= esc($error) ?></li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php if ($scoresLocked): ?>
|
||||
<div class="alert alert-warning">
|
||||
Scores are locked for this class. Unlock to edit.
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<div class="d-flex justify-content-between mt-4 flex-wrap gap-2">
|
||||
<button type="submit" class="btn btn-success" <?= $lockAttr ?>>
|
||||
<i class="bi bi-save"></i> Save All Changes
|
||||
</button>
|
||||
<a href="<?= base_url('/grading') ?>" class="btn btn-secondary">
|
||||
<i class="bi bi-x-circle"></i> Back to Scores
|
||||
</a>
|
||||
|
||||
</div>
|
||||
<br>
|
||||
<form id="commentsForm" action="<?= base_url('grading/updateComments') ?>" method="post">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="semester" value="<?= esc($semester) ?>">
|
||||
<input type="hidden" name="school_year" value="<?= esc($schoolYear) ?>">
|
||||
<input type="hidden" name="class_section_id" value="<?= esc($classSectionId) ?>">
|
||||
<input type="hidden" id="proofreadCsrfName" value="<?= csrf_token() ?>">
|
||||
<input type="hidden" id="proofreadCsrfHash" value="<?= csrf_hash() ?>">
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-3 flex-wrap gap-2">
|
||||
<div>
|
||||
<button type="button" id="proofreadAllBtn" class="btn btn-outline-primary btn-sm" <?= $lockAttr ?>>
|
||||
<i class="bi bi-magic"></i> Proofread All Reviews
|
||||
</button>
|
||||
</div>
|
||||
<div class="text-muted small" id="proofreadAllStatus"></div>
|
||||
</div>
|
||||
|
||||
<div class="table-responsive w-100">
|
||||
<table id="commentsTable" class="table table-bordered table-striped w-100 comments-table" data-no-mgmt-sticky>
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>School ID</th>
|
||||
<th>Student Name</th>
|
||||
<th>PTAP Comment</th>
|
||||
<th>PTAP Review</th>
|
||||
<?php if ($semester === 'Fall'): ?>
|
||||
<th>Midterm Comment</th>
|
||||
<th>Midterm Review</th>
|
||||
<?php elseif ($semester === 'Spring'): ?>
|
||||
<th>Final Comment</th>
|
||||
<th>Final Review</th>
|
||||
<?php endif; ?>
|
||||
<th>Attendance Comment</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php $rowNum = 1; ?>
|
||||
<?php foreach ($students as $student): ?>
|
||||
<?php $studentId = $student['student_id']; ?>
|
||||
<tr>
|
||||
<td><?= $rowNum++ ?></td>
|
||||
<td><?= esc($student['school_id']) ?></td>
|
||||
<td>
|
||||
<a href="#" class="text-decoration-none" data-family-student-id="<?= (int)$studentId ?>">
|
||||
<?= esc($student['firstname']) ?> <?= esc($student['lastname']) ?>
|
||||
</a>
|
||||
</td>
|
||||
|
||||
<!-- PTAP Comment and Review -->
|
||||
<td>
|
||||
<?php $ptapCommentId = 'ptap-comment-' . $studentId; ?>
|
||||
<?php $ptapReviewId = 'ptap-review-' . $studentId; ?>
|
||||
<textarea
|
||||
id="<?= esc($ptapCommentId) ?>"
|
||||
name="comments[<?= $studentId ?>][ptap]"
|
||||
rows="2"
|
||||
class="form-control"
|
||||
data-first-name="<?= esc($student['firstname']) ?>"
|
||||
minlength="100"
|
||||
maxlength="350"
|
||||
placeholder="Enter PTAP comment" <?= $lockAttr ?>><?= esc($comments[$studentId]['ptap']['comment'] ?? '') ?></textarea>
|
||||
</td>
|
||||
<td class="ptap-review-cell">
|
||||
<div class="d-flex gap-2 align-items-start">
|
||||
<textarea
|
||||
id="<?= esc($ptapReviewId) ?>"
|
||||
name="reviews[<?= $studentId ?>][ptap]"
|
||||
rows="2"
|
||||
class="form-control review-field"
|
||||
placeholder="Enter PTAP review" <?= $lockAttr ?>><?= esc($comments[$studentId]['ptap']['comment_review'] ?? '') ?></textarea>
|
||||
<div class="d-flex flex-column gap-1 align-items-stretch">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-outline-secondary btn-sm copy-ptap-btn"
|
||||
data-copy-source="#<?= esc($ptapCommentId) ?>"
|
||||
data-copy-target="#<?= esc($ptapReviewId) ?>" <?= $lockAttr ?>>
|
||||
Paste
|
||||
</button>
|
||||
<div class="small text-muted proofread-status" data-status-for="<?= esc($ptapReviewId) ?>"></div>
|
||||
<ul class="proofread-list small mb-0" data-list-for="<?= esc($ptapReviewId) ?>"></ul>
|
||||
</div>
|
||||
</div>
|
||||
<?php if (isset($comments[$studentId]['ptap']['reviewed_by'])): ?>
|
||||
<div class="text-muted small mt-1">
|
||||
Last reviewed by: <?= esc($comments[$studentId]['ptap']['reviewed_by']) ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
|
||||
<!-- Conditional Midterm (Fall) or Final (Spring) -->
|
||||
<?php if ($semester === 'Fall'): ?>
|
||||
<td>
|
||||
<?php $midCommentId = 'midterm-comment-' . $studentId; ?>
|
||||
<?php $midReviewId = 'midterm-review-' . $studentId; ?>
|
||||
<textarea
|
||||
id="<?= esc($midCommentId) ?>"
|
||||
name="comments[<?= $studentId ?>][midterm]"
|
||||
rows="2"
|
||||
class="form-control"
|
||||
data-first-name="<?= esc($student['firstname']) ?>"
|
||||
minlength="100"
|
||||
maxlength="350"
|
||||
placeholder="Enter Midterm comment" <?= $lockAttr ?>><?= esc($comments[$studentId]['midterm']['comment'] ?? '') ?></textarea>
|
||||
</td>
|
||||
<td>
|
||||
<div class="d-flex gap-2 align-items-start">
|
||||
<textarea
|
||||
id="<?= esc($midReviewId) ?>"
|
||||
name="reviews[<?= $studentId ?>][midterm]"
|
||||
rows="2"
|
||||
class="form-control review-field"
|
||||
data-first-name="<?= esc($student['firstname']) ?>"
|
||||
minlength="100"
|
||||
maxlength="350"
|
||||
placeholder="Enter Midterm review" <?= $lockAttr ?>><?= esc($comments[$studentId]['midterm']['comment_review'] ?? '') ?></textarea>
|
||||
<div class="d-flex flex-column gap-1 align-items-stretch">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-outline-secondary btn-sm copy-midterm-btn"
|
||||
data-copy-source="#<?= esc($midCommentId) ?>"
|
||||
data-copy-target="#<?= esc($midReviewId) ?>" <?= $lockAttr ?>>
|
||||
Paste
|
||||
</button>
|
||||
<div class="small text-muted proofread-status" data-status-for="<?= esc($midReviewId) ?>"></div>
|
||||
<ul class="proofread-list small mb-0" data-list-for="<?= esc($midReviewId) ?>"></ul>
|
||||
</div>
|
||||
</div>
|
||||
<?php if (isset($comments[$studentId]['midterm']['reviewed_by'])): ?>
|
||||
<div class="text-muted small mt-1">
|
||||
Last reviewed by: <?= esc($comments[$studentId]['midterm']['reviewed_by']) ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<?php elseif ($semester === 'Spring'): ?>
|
||||
<td>
|
||||
<textarea
|
||||
name="comments[<?= $studentId ?>][final]"
|
||||
rows="2"
|
||||
class="form-control"
|
||||
data-first-name="<?= esc($student['firstname']) ?>"
|
||||
minlength="100"
|
||||
maxlength="350"
|
||||
placeholder="Enter Final comment" <?= $lockAttr ?>><?= esc($comments[$studentId]['final']['comment'] ?? '') ?></textarea>
|
||||
</td>
|
||||
<?php $finalReviewId = 'final-review-' . $studentId; ?>
|
||||
<td>
|
||||
<div class="d-flex gap-2 align-items-start">
|
||||
<textarea
|
||||
id="<?= esc($finalReviewId) ?>"
|
||||
name="reviews[<?= $studentId ?>][final]"
|
||||
rows="2"
|
||||
class="form-control review-field"
|
||||
placeholder="Enter Final review" <?= $lockAttr ?>><?= esc($comments[$studentId]['final']['comment_review'] ?? '') ?></textarea>
|
||||
<div class="d-flex flex-column gap-1 align-items-stretch">
|
||||
<div class="small text-muted proofread-status" data-status-for="<?= esc($finalReviewId) ?>"></div>
|
||||
<ul class="proofread-list small mb-0" data-list-for="<?= esc($finalReviewId) ?>"></ul>
|
||||
</div>
|
||||
</div>
|
||||
<?php if (isset($comments[$studentId]['final']['reviewed_by'])): ?>
|
||||
<div class="text-muted small mt-1">
|
||||
Last reviewed by: <?= esc($comments[$studentId]['final']['reviewed_by']) ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<?php endif; ?>
|
||||
<!-- Attendance Comment only -->
|
||||
<td>
|
||||
<textarea
|
||||
name="comments[<?= $studentId ?>][attendance]"
|
||||
rows="2"
|
||||
class="form-control"
|
||||
data-first-name="<?= esc($student['firstname']) ?>"
|
||||
minlength="100"
|
||||
maxlength="350"
|
||||
placeholder="Enter Attendance comment" <?= $lockAttr ?>><?= esc($comments[$studentId]['attendance']['comment'] ?? '') ?></textarea>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-between mt-4 flex-wrap gap-2">
|
||||
<button type="submit" class="btn btn-success" <?= $lockAttr ?>>
|
||||
<i class="bi bi-save"></i> Save All Changes
|
||||
</button>
|
||||
<a href="<?= base_url('/grading') ?>" class="btn btn-secondary">
|
||||
<i class="bi bi-x-circle"></i> Back to Scores
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.review-field {
|
||||
background-color: #f8f9fa;
|
||||
border-left: 3px solid #dee2e6;
|
||||
}
|
||||
.review-field:focus {
|
||||
background-color: #fff;
|
||||
border-left: 3px solid #86b7fe;
|
||||
}
|
||||
textarea.form-control {
|
||||
min-height: 80px;
|
||||
}
|
||||
.text-muted small {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
.comments-table {
|
||||
width: 100%;
|
||||
table-layout: auto;
|
||||
}
|
||||
.table-responsive {
|
||||
width: 100%;
|
||||
overflow-x: auto;
|
||||
}
|
||||
/* Stretch table to available space */
|
||||
.comments-table th,
|
||||
.comments-table td {
|
||||
white-space: nowrap;
|
||||
}
|
||||
.proofread-list {
|
||||
list-style: none;
|
||||
padding-left: 0;
|
||||
margin-bottom: 0;
|
||||
white-space: normal;
|
||||
}
|
||||
.proofread-status {
|
||||
min-height: 1.25rem;
|
||||
white-space: normal;
|
||||
}
|
||||
</style>
|
||||
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
<?= $this->section('scripts') ?>
|
||||
<script>
|
||||
window.PROOFREAD_ENDPOINT = "<?= site_url('api/proofread') ?>";
|
||||
</script>
|
||||
<script src="<?= base_url('assets/js/proofread.js') ?>"></script>
|
||||
<script>
|
||||
// Attach DataTables sorting for comments table
|
||||
(function ($) {
|
||||
function initCommentsTable() {
|
||||
if (!($ && $.fn && $.fn.DataTable)) return;
|
||||
const $table = $('#commentsTable');
|
||||
if (!$table.length) return;
|
||||
|
||||
$table.DataTable({
|
||||
paging: false,
|
||||
info: false,
|
||||
searching: false,
|
||||
autoWidth: false,
|
||||
order: [[2, 'asc']], // Student Name
|
||||
columnDefs: [
|
||||
{ targets: 0, orderable: false, searchable: false },
|
||||
{ targets: 1, className: 'text-nowrap' },
|
||||
],
|
||||
fixedHeader: {
|
||||
header: true,
|
||||
headerOffset: typeof getFixedHeaderOffset === 'function' ? getFixedHeaderOffset() : 0,
|
||||
},
|
||||
drawCallback: function () {
|
||||
const api = this.api();
|
||||
api.column(0, { search: 'applied', order: 'applied' }).nodes().each(function (cell, i) {
|
||||
cell.textContent = i + 1;
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
$(document).ready(initCommentsTable);
|
||||
})(jQuery);
|
||||
</script>
|
||||
<script>
|
||||
// Auto-expand textareas on load and input
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
document.querySelectorAll('textarea').forEach(textarea => {
|
||||
textarea.style.height = 'auto';
|
||||
textarea.style.height = (textarea.scrollHeight) + 'px';
|
||||
|
||||
textarea.addEventListener('input', function() {
|
||||
this.style.height = 'auto';
|
||||
this.style.height = (this.scrollHeight) + 'px';
|
||||
});
|
||||
});
|
||||
|
||||
const form = document.getElementById('commentsForm');
|
||||
if (form) {
|
||||
const escapeRegex = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
const csrfInput = form.querySelector('input[type="hidden"][name*="csrf"]');
|
||||
const csrfCookieName = '<?= esc(config('Security')->csrfCookieName ?? (config('Security')->cookieName ?? 'csrf_cookie_name')) ?>';
|
||||
|
||||
const getCookie = function(name) {
|
||||
try {
|
||||
const parts = document.cookie.split(';').map(function(value) { return value.trim(); });
|
||||
const match = parts.find(function(value) { return value.indexOf(name + '=') === 0; });
|
||||
return match ? match.split('=')[1] : '';
|
||||
} catch (e) {
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
const syncCsrfToken = function() {
|
||||
if (!csrfInput) return;
|
||||
const fresh = decodeURIComponent(getCookie(csrfCookieName) || '');
|
||||
if (fresh) csrfInput.value = fresh;
|
||||
};
|
||||
|
||||
const parseFieldName = function(name) {
|
||||
const match = /^(comments|reviews)\[(\d+)\]\[([^\]]+)\]$/.exec(name || '');
|
||||
if (!match) return null;
|
||||
return { group: match[1], studentId: match[2], scoreType: match[3] };
|
||||
};
|
||||
|
||||
const isValidComment = function(field, value) {
|
||||
if (!value) return false;
|
||||
if (value.length < 100 || value.length > 350) return false;
|
||||
const firstName = field.dataset.firstName || '';
|
||||
if (!firstName) return true;
|
||||
const startsWithName = new RegExp(`^${escapeRegex(firstName)}\\b`, 'i');
|
||||
return startsWithName.test(value);
|
||||
};
|
||||
|
||||
const shouldAutoSave = function(field, value) {
|
||||
if (field.disabled) return false;
|
||||
const lastSaved = field.getAttribute('data-last-saved') || '';
|
||||
if (value === lastSaved) return false;
|
||||
const parsed = parseFieldName(field.name || '');
|
||||
if (!parsed) return false;
|
||||
if (parsed.group === 'comments') {
|
||||
return isValidComment(field, value);
|
||||
}
|
||||
return value !== '';
|
||||
};
|
||||
|
||||
const buildAutoSavePayload = function(field) {
|
||||
const parsed = parseFieldName(field.name || '');
|
||||
if (!parsed) return null;
|
||||
|
||||
const data = new FormData();
|
||||
if (csrfInput) {
|
||||
const csrfValue = decodeURIComponent(getCookie(csrfCookieName) || '') || csrfInput.value;
|
||||
data.append(csrfInput.name, csrfValue);
|
||||
}
|
||||
['semester', 'school_year', 'class_section_id'].forEach(function(name) {
|
||||
const input = form.querySelector('[name="' + name + '"]');
|
||||
if (input) data.append(name, input.value);
|
||||
});
|
||||
|
||||
if (parsed.group === 'comments') {
|
||||
data.append(field.name, field.value);
|
||||
} else if (parsed.group === 'reviews') {
|
||||
const commentName = `comments[${parsed.studentId}][${parsed.scoreType}]`;
|
||||
const commentField = form.querySelector(`[name="${commentName}"]`);
|
||||
data.append(commentName, commentField ? commentField.value : '');
|
||||
data.append(field.name, field.value);
|
||||
}
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
const autoSaveField = function(field) {
|
||||
const value = field.value.trim();
|
||||
if (!shouldAutoSave(field, value)) return;
|
||||
const payload = buildAutoSavePayload(field);
|
||||
if (!payload) return;
|
||||
fetch(form.action, {
|
||||
method: 'POST',
|
||||
body: payload,
|
||||
credentials: 'same-origin'
|
||||
})
|
||||
.then(function() {
|
||||
field.setAttribute('data-last-saved', value);
|
||||
syncCsrfToken();
|
||||
})
|
||||
.catch(function() {});
|
||||
};
|
||||
|
||||
form.addEventListener('submit', function(event) {
|
||||
syncCsrfToken();
|
||||
const errors = [];
|
||||
form.querySelectorAll('textarea[data-first-name]').forEach(field => {
|
||||
const value = field.value.trim();
|
||||
if (value === '') return;
|
||||
|
||||
const min = 100;
|
||||
const max = 350;
|
||||
const firstName = field.dataset.firstName || 'student';
|
||||
|
||||
if (value.length < min || value.length > max) {
|
||||
errors.push(`Comment for ${firstName} must be between ${min} and ${max} characters.`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (field.dataset.firstName) {
|
||||
const startsWithName = new RegExp(`^${escapeRegex(field.dataset.firstName)}\\b`, 'i');
|
||||
if (!startsWithName.test(value)) {
|
||||
errors.push(`Comment for ${firstName} must start with the student's first name.`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (errors.length > 0) {
|
||||
event.preventDefault();
|
||||
alert(errors.join('\\n'));
|
||||
}
|
||||
});
|
||||
|
||||
form.querySelectorAll('textarea[name^="comments["], textarea[name^="reviews["]').forEach(function(field) {
|
||||
field.setAttribute('data-last-saved', field.value.trim());
|
||||
field.addEventListener('blur', function() {
|
||||
autoSaveField(this);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const wireCopyButtons = (selector) => {
|
||||
document.querySelectorAll(selector).forEach(button => {
|
||||
button.addEventListener('click', function() {
|
||||
const source = document.querySelector(this.dataset.copySource || '');
|
||||
const target = document.querySelector(this.dataset.copyTarget || '');
|
||||
if (!source || !target) return;
|
||||
target.value = source.value;
|
||||
target.style.height = 'auto';
|
||||
target.style.height = (target.scrollHeight) + 'px';
|
||||
target.focus();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
document.querySelectorAll('.copy-ptap-btn').forEach(button => {
|
||||
button.addEventListener('click', function() {
|
||||
const source = document.querySelector(this.dataset.copySource || '');
|
||||
const target = document.querySelector(this.dataset.copyTarget || '');
|
||||
if (!source || !target) return;
|
||||
target.value = source.value;
|
||||
target.style.height = 'auto';
|
||||
target.style.height = (target.scrollHeight) + 'px';
|
||||
target.focus();
|
||||
});
|
||||
});
|
||||
wireCopyButtons('.copy-midterm-btn');
|
||||
});
|
||||
</script>
|
||||
<?= $this->endSection() ?>
|
||||
@@ -0,0 +1,128 @@
|
||||
<?php
|
||||
// views/grading/homework.php, quiz.php, project.php, test.php, midterm.php, final.php, comments.php
|
||||
// Use this as a shared structure but each type will set its own unique variables below
|
||||
|
||||
// Set title based on the view file
|
||||
$viewFile = basename(__FILE__, '.php');
|
||||
|
||||
switch ($viewFile) {
|
||||
case 'homework':
|
||||
$type = 'homework';
|
||||
$title = 'Homework';
|
||||
break;
|
||||
case 'quiz':
|
||||
$type = 'quiz';
|
||||
$title = 'Quiz';
|
||||
break;
|
||||
case 'project':
|
||||
$type = 'project';
|
||||
$title = 'Project';
|
||||
break;
|
||||
case 'test':
|
||||
$type = 'test';
|
||||
$title = 'Test';
|
||||
break;
|
||||
case 'midterm':
|
||||
$type = 'midterm';
|
||||
$title = 'Midterm Exam';
|
||||
break;
|
||||
case 'final':
|
||||
$type = 'final';
|
||||
$title = 'Final Exam';
|
||||
break;
|
||||
case 'comments':
|
||||
$type = 'comments';
|
||||
$title = 'General Comments';
|
||||
break;
|
||||
default:
|
||||
$type = 'homework';
|
||||
$title = 'Homework';
|
||||
}
|
||||
|
||||
$scoresLocked = !empty($scoresLocked);
|
||||
$lockAttr = $scoresLocked ? 'disabled' : '';
|
||||
?>
|
||||
|
||||
<?= $this->extend('layout/management_layout') ?>
|
||||
<?= $this->section('content') ?>
|
||||
<div class="container-xxl py-5">
|
||||
<div class="container">
|
||||
<div class="text-center mx-auto mb-5 wow fadeInUp" data-wow-delay="0.1s" style="max-width: 600px;">
|
||||
<br>
|
||||
<h2 class="text-dark" style="font-family: Arial, sans-serif;">Final Exam Scores</h2>
|
||||
</div>
|
||||
|
||||
<?php if (session()->get('status')): ?>
|
||||
<div class="alert alert-success">
|
||||
<?= session()->get('status') ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php if ($scoresLocked): ?>
|
||||
<div class="alert alert-warning">
|
||||
Scores are locked for this class. Unlock to edit.
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<div class="d-flex justify-content-between mt-4 flex-wrap gap-2">
|
||||
<button type="submit" class="btn btn-success" <?= $lockAttr ?>>
|
||||
<i class="bi bi-save"></i> Save Changes
|
||||
</button>
|
||||
<a href="<?= base_url('/grading') ?>" class="btn btn-secondary">
|
||||
<i class="bi bi-x-circle"></i> Back to Scores
|
||||
</a>
|
||||
</div>
|
||||
<form action="<?= base_url('grading/updateFinalExamScores') ?>" method="post"
|
||||
onsubmit="return validateForm()">
|
||||
<?= csrf_field() ?>
|
||||
|
||||
<input type="hidden" name="semester" value="<?= esc($semester) ?>">
|
||||
<input type="hidden" name="school_year" value="<?= esc($schoolYear) ?>">
|
||||
|
||||
<table class="table table-bordered mt-4" data-no-mgmt-sticky>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>School ID</th>
|
||||
<th>First Name</th>
|
||||
<th>Last Name</th>
|
||||
<th class="text-center">Final Exam Score</th>
|
||||
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($students as $index => $student): ?>
|
||||
<tr>
|
||||
<td><?= $index + 1 ?></td>
|
||||
<td><?= esc($student['school_id']) ?></td>
|
||||
<td>
|
||||
<a href="#" class="text-decoration-none" data-family-student-id="<?= (int)($student['student_id'] ?? 0) ?>">
|
||||
<?= esc($student['firstname']) ?>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="#" class="text-decoration-none" data-family-student-id="<?= (int)($student['student_id'] ?? 0) ?>">
|
||||
<?= esc($student['lastname']) ?>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<input type="number" name="final_score[<?= esc($student['student_id']) ?>][score]"
|
||||
value="<?= esc($student['score'] ?? '') ?>" class="form-control text-center" min="0" max="100" step="0.01"
|
||||
placeholder="Enter score (optional)" <?= $lockAttr ?>>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="d-flex justify-content-between mt-4 flex-wrap gap-2">
|
||||
<button type="submit" class="btn btn-success" <?= $lockAttr ?>>
|
||||
<i class="bi bi-save"></i> Save Changes
|
||||
</button>
|
||||
<a href="<?= base_url('/grading') ?>" class="btn btn-secondary">
|
||||
<i class="bi bi-x-circle"></i> Back to Scores
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<br>
|
||||
</div>
|
||||
<?= $this->endSection() ?>
|
||||
@@ -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 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() ?>
|
||||
@@ -0,0 +1,167 @@
|
||||
<?= $this->extend('layout/management_layout') ?>
|
||||
<?= $this->section('content') ?>
|
||||
|
||||
<?php
|
||||
$title = 'Update Homework Scores';
|
||||
$type = 'homework';
|
||||
$scoresLocked = !empty($scoresLocked);
|
||||
$lockAttr = $scoresLocked ? 'disabled' : '';
|
||||
?>
|
||||
|
||||
<div class="container-fluid py-5 px-3 px-lg-4">
|
||||
<div class="text-center mx-auto mb-4 mt-3" style="max-width: 600px;">
|
||||
<h2 class="text-dark" style="font-family: Arial, sans-serif;"><?= esc($title) ?></h2>
|
||||
</div>
|
||||
|
||||
<?php if (session()->get('status')): ?>
|
||||
<div class="alert alert-success">
|
||||
<?= session()->get('status') ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php if ($scoresLocked): ?>
|
||||
<div class="alert alert-warning">
|
||||
Scores are locked for this class. Unlock to edit.
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<div class="d-flex justify-content-between mt-4 flex-wrap gap-2">
|
||||
<button type="submit" class="btn btn-success" <?= $lockAttr ?>>
|
||||
<i class="bi bi-save"></i> Save Changes
|
||||
</button>
|
||||
<a href="<?= base_url('/grading') ?>" class="btn btn-secondary">
|
||||
<i class="bi bi-x-circle"></i> Back to Scores
|
||||
</a>
|
||||
</div>
|
||||
<form action="<?= base_url('/grading/updateHomework') ?>" method="post">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="semester" value="<?= esc($semester) ?>">
|
||||
<input type="hidden" name="school_year" value="<?= esc($schoolYear) ?>">
|
||||
<input type="hidden" name="class_section_id" value="<?= esc($classSectionId ?? '') ?>">
|
||||
|
||||
<table id="homeworkTable" class="table table-bordered mt-4 w-100" data-no-mgmt-sticky>
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>School ID</th>
|
||||
<th>First Name</th>
|
||||
<th>Last Name</th>
|
||||
<?php foreach ($homeworkHeaders as $index): ?>
|
||||
<th class="text-center"><?= esc("Homework " . $index) ?></th>
|
||||
<?php endforeach; ?>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php $row = 1; ?>
|
||||
<?php foreach ($students as $student): ?>
|
||||
<?php
|
||||
$studentId = $student['id'];
|
||||
$scores = $homeworkScores[$studentId]['scores'] ?? [];
|
||||
?>
|
||||
<tr>
|
||||
<td><?= $row++ ?></td>
|
||||
<td><?= esc($student['school_id']) ?></td>
|
||||
<td>
|
||||
<a href="#" class="text-decoration-none" data-family-student-id="<?= (int)$studentId ?>">
|
||||
<?= esc($student['firstname']) ?>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="#" class="text-decoration-none" data-family-student-id="<?= (int)$studentId ?>">
|
||||
<?= esc($student['lastname']) ?>
|
||||
</a>
|
||||
</td>
|
||||
<input type="hidden" name="student_ids[]" value="<?= esc($studentId) ?>">
|
||||
|
||||
<?php foreach ($homeworkHeaders as $index): ?>
|
||||
<?php $scoreValue = $scores[$index] ?? ''; ?>
|
||||
<td class="<?= $scoreValue === '' ? 'bg-warning-subtle' : '' ?>">
|
||||
<input type="number"
|
||||
name="scores[<?= $studentId ?>][<?= $index ?>]"
|
||||
value="<?= esc(is_array($scoreValue) ? '' : $scoreValue) ?>"
|
||||
class="form-control text-center"
|
||||
min="0" max="100" step="0.01" <?= $lockAttr ?>>
|
||||
</td>
|
||||
<?php endforeach; ?>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
|
||||
</table>
|
||||
|
||||
<div class="d-flex justify-content-between mt-4 flex-wrap gap-2">
|
||||
<button type="submit" class="btn btn-success" <?= $lockAttr ?>>
|
||||
<i class="bi bi-save"></i> Save Changes
|
||||
</button>
|
||||
<a href="<?= base_url('/grading') ?>" class="btn btn-secondary">
|
||||
<i class="bi bi-x-circle"></i> Back to Scores
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
<?= $this->section('scripts') ?>
|
||||
<script>
|
||||
(function ($) {
|
||||
// Use the live value inside number inputs for ordering
|
||||
if ($ && $.fn && $.fn.dataTable && !$.fn.dataTable.ext.order['dom-num-input']) {
|
||||
$.fn.dataTable.ext.order['dom-num-input'] = function (settings, col) {
|
||||
return this.api()
|
||||
.column(col, { order: 'index' })
|
||||
.nodes()
|
||||
.map(function (td) {
|
||||
const val = $('input', td).val();
|
||||
const num = parseFloat(val);
|
||||
return Number.isFinite(num) ? num : -Infinity; // empty/invalid go to bottom on desc
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function initHomeworkTable() {
|
||||
if (!($ && $.fn && $.fn.DataTable)) return;
|
||||
|
||||
const $table = $('#homeworkTable');
|
||||
if (!$table.length) return;
|
||||
|
||||
// Update data-order whenever a score changes so redraws stay accurate
|
||||
$table.on('input', 'input[type="number"]', function () {
|
||||
const num = parseFloat(this.value);
|
||||
this.closest('td')?.setAttribute('data-order', Number.isFinite(num) ? num : -Infinity);
|
||||
});
|
||||
|
||||
// Identify score columns (all columns after last name)
|
||||
const totalCols = $table.find('thead th').length;
|
||||
const scoreCols = [];
|
||||
for (let i = 4; i < totalCols; i++) scoreCols.push(i);
|
||||
|
||||
$table.DataTable({
|
||||
paging: false,
|
||||
info: false,
|
||||
searching: false,
|
||||
autoWidth: false,
|
||||
order: [[2, 'asc'], [3, 'asc']],
|
||||
columnDefs: [
|
||||
{ targets: 0, orderable: false, searchable: false },
|
||||
{ targets: 1, className: 'text-nowrap' },
|
||||
...(scoreCols.length ? [{ targets: scoreCols, orderDataType: 'dom-num-input', orderSequence: ['desc', 'asc'], type: 'num' }] : []),
|
||||
],
|
||||
fixedHeader: {
|
||||
header: true,
|
||||
headerOffset: typeof getFixedHeaderOffset === 'function' ? getFixedHeaderOffset() : 0,
|
||||
},
|
||||
drawCallback: function () {
|
||||
const api = this.api();
|
||||
api
|
||||
.column(0, { search: 'applied', order: 'applied' })
|
||||
.nodes()
|
||||
.each(function (cell, i) {
|
||||
cell.textContent = i + 1;
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
$(document).ready(initHomeworkTable);
|
||||
})(jQuery);
|
||||
</script>
|
||||
<?= $this->endSection() ?>
|
||||
@@ -0,0 +1,230 @@
|
||||
<?= $this->extend('layout/management_layout') ?>
|
||||
<?= $this->section('content') ?>
|
||||
|
||||
<?php
|
||||
// Helpers for labels
|
||||
$labelFor = function (int $cid): string {
|
||||
if ($cid === 13) return 'KG';
|
||||
if ($cid === 12) return 'Youth';
|
||||
if ($cid >= 1 && $cid <= 11) return 'Grade ' . $cid;
|
||||
return 'Class ' . $cid;
|
||||
};
|
||||
|
||||
// Pre-format header dates for display
|
||||
$headerDates = array_map(function ($ymd) {
|
||||
$dt = \DateTime::createFromFormat('Y-m-d', $ymd, new \DateTimeZone('UTC'));
|
||||
return $dt ? $dt->format('m-d-Y') : $ymd;
|
||||
}, $sundays ?? []);
|
||||
$todayYmd = local_date(utc_now(), 'Y-m-d');
|
||||
?>
|
||||
|
||||
<div class="container-fluid py-3">
|
||||
<div class="px-3">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3 flex-wrap gap-2">
|
||||
<h2 class="text-center mt-4 mb-3">Homework Tracking</h2>
|
||||
<div class="text-muted"><?= esc(ucfirst($semester ?? '')) ?> • <?= esc($schoolYear ?? '') ?></div>
|
||||
</div>
|
||||
|
||||
<div class="mb-2 small">
|
||||
<span class="badge text-bg-success">Entered</span>
|
||||
<span class="badge text-bg-danger">Missing</span>
|
||||
<span class="badge text-bg-warning">No School</span>
|
||||
<span class="badge bg-future text-dark border">Future</span>
|
||||
</div>
|
||||
|
||||
<?php if (empty($teachers)): ?>
|
||||
<div class="alert alert-light border">No teacher assignments found for the current term.</div>
|
||||
<?php else: ?>
|
||||
<?php
|
||||
$page = max(1, (int)($page ?? 1));
|
||||
$totalPages = max(1, (int)($totalPages ?? 1));
|
||||
$baseUrl = base_url('grading/homework-tracking');
|
||||
$mkHref = function($p) use ($baseUrl) { return $baseUrl . '?page=' . (int)$p; };
|
||||
?>
|
||||
|
||||
<div class="d-flex justify-content-end mb-2 gap-2">
|
||||
<div class="btn-group" role="group" aria-label="Pagination top">
|
||||
<a class="btn btn-outline-secondary <?= $page <= 1 ? 'disabled' : '' ?>" href="<?= $mkHref(1) ?>">First</a>
|
||||
<a class="btn btn-outline-secondary <?= $page <= 1 ? 'disabled' : '' ?>" href="<?= $mkHref(max(1, $page-1)) ?>">Prev</a>
|
||||
<span class="btn btn-outline-secondary disabled">Page <?= $page ?> / <?= $totalPages ?></span>
|
||||
<a class="btn btn-outline-secondary <?= $page >= $totalPages ? 'disabled' : '' ?>" href="<?= $mkHref(min($totalPages, $page+1)) ?>">Next</a>
|
||||
<a class="btn btn-outline-secondary <?= $page >= $totalPages ? 'disabled' : '' ?>" href="<?= $mkHref($totalPages) ?>">Last</a>
|
||||
</div>
|
||||
</div>
|
||||
<?= $this->section('styles') ?>
|
||||
<style>
|
||||
/* Header is handled by JS polyfill to avoid covering content; keep a light bg */
|
||||
.table-sticky thead th { background: var(--bs-light, #f8f9fa) !important; }
|
||||
.table-sticky .sticky-col { position: sticky; left: 0; z-index: 5; background: var(--bs-table-bg, #fff); }
|
||||
/* Use a CSS variable for the second sticky column's left offset so it can adapt to content width */
|
||||
.table-sticky .sticky-col-2 { position: sticky; left: var(--sticky-left-2, 240px); z-index: 5; background: var(--bs-table-bg, #fff); }
|
||||
.table-sticky th.sticky-col, .table-sticky th.sticky-col-2 { z-index: 8; }
|
||||
.table-sticky td, .table-sticky th { white-space: nowrap; }
|
||||
.bg-future { background-color: #e7f1ff !important; }
|
||||
</style>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table id="homeworkTrackTable" class="table table-bordered align-middle table-sticky no-mgmt-sticky">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th class="sticky-col">Grade</th>
|
||||
<th class="sticky-col-2" style="min-width:300px; width:300px;">Teacher & TAs</th>
|
||||
<?php foreach (($headerDates ?? []) as $i => $label): $ymd = $sundays[$i] ?? ''; ?>
|
||||
<?php $isCal = !empty($eventDays[$ymd]); $isFuture = ($ymd > $todayYmd); ?>
|
||||
<th class="text-center <?= $isCal ? 'bg-warning' : ($isFuture ? 'bg-future' : '') ?>" title="<?= esc($ymd) ?>"><?= esc($label) ?></th>
|
||||
<?php endforeach; ?>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($teachers as $row): ?>
|
||||
<?php
|
||||
$cid = (int)($row['class_id'] ?? 0);
|
||||
$csid = (int)($row['class_section_id'] ?? 0);
|
||||
$cname = (string)($row['class_section_name'] ?? '');
|
||||
$teacherNames = (array)($row['teachers'] ?? []);
|
||||
$taNames = (array)($row['tas'] ?? []);
|
||||
?>
|
||||
<tr>
|
||||
<td class="sticky-col" style="background: var(--bs-table-bg, #fff);">
|
||||
<?= esc($labelFor($cid)) ?>
|
||||
<?php if (!empty($cname)): ?>
|
||||
<div class="text-muted small">Section: <?= esc($cname) ?></div>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td class="sticky-col-2" style="min-width:300px; width:300px; background: var(--bs-table-bg, #fff);">
|
||||
<?php if (!empty($teacherNames)): ?>
|
||||
<div><strong>Teacher<?= count($teacherNames) > 1 ? 's' : '' ?>:</strong> <?= esc(implode(', ', $teacherNames)) ?></div>
|
||||
<?php else: ?>
|
||||
<div><strong>Teacher:</strong> N/A</div>
|
||||
<?php endif; ?>
|
||||
<div><strong>TA<?= count($taNames) !== 1 ? 's' : '' ?>:</strong> <?= !empty($taNames) ? esc(implode(', ', $taNames)) : '—' ?></div>
|
||||
</td>
|
||||
<?php foreach (($sundays ?? []) as $ymd): ?>
|
||||
<?php if (!empty($eventDays[$ymd])): ?>
|
||||
<td class="bg-warning text-center">—</td>
|
||||
<?php else: ?>
|
||||
<?php $isFuture = ($ymd > $todayYmd); ?>
|
||||
<?php if ($isFuture): ?>
|
||||
<td class="text-center bg-future">—</td>
|
||||
<?php else: ?>
|
||||
<?php $ok = !empty($hasHomeworkByDate[$csid][$ymd] ?? null); ?>
|
||||
<?php if ($ok): ?>
|
||||
<?php
|
||||
$rawDate = $hwEnteredAtByDate[$csid][$ymd] ?? '';
|
||||
$disp = '';
|
||||
if ($rawDate) { $disp = local_date($rawDate, 'm-d-Y'); }
|
||||
?>
|
||||
<td class="text-center text-white" style="background-color:#198754;"><?= esc($disp ?: '✓') ?></td>
|
||||
<?php else: ?>
|
||||
<td class="text-center text-white" style="background-color:#dc3545;">✖</td>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
<?php endforeach; ?>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<?= $this->section('scripts') ?>
|
||||
<script>
|
||||
(function(){
|
||||
function updateStickyOffsets(){
|
||||
document.querySelectorAll('table.table-sticky').forEach(function(tbl){
|
||||
const firstHeader = tbl.querySelector('thead th.sticky-col');
|
||||
const firstCell = tbl.querySelector('tbody td.sticky-col');
|
||||
const ref = firstHeader || firstCell;
|
||||
if (!ref) return;
|
||||
const width = ref.offsetWidth || ref.getBoundingClientRect().width || 240;
|
||||
tbl.style.setProperty('--sticky-left-2', width + 'px');
|
||||
});
|
||||
}
|
||||
window.addEventListener('load', updateStickyOffsets);
|
||||
window.addEventListener('resize', updateStickyOffsets);
|
||||
})();
|
||||
</script>
|
||||
<script>
|
||||
// JS sticky header polyfill for homework tracking
|
||||
(function(){
|
||||
function getFixedHeaderOffset(){
|
||||
var total = 0;
|
||||
try {
|
||||
var sels = ['header.navbar.sticky-top','header.navbar.fixed-top','#navbarManagement.navbar.sticky-top','#navbarManagement.navbar.fixed-top'];
|
||||
var seen = [];
|
||||
sels.forEach(function(s){ document.querySelectorAll(s).forEach(function(el){ if (seen.indexOf(el) === -1) seen.push(el); }); });
|
||||
document.querySelectorAll('.navbar.sticky-top, .navbar.fixed-top').forEach(function(el){ if (seen.indexOf(el) === -1) seen.push(el); });
|
||||
seen.forEach(function(el){ var h = el.offsetHeight || (el.getBoundingClientRect && el.getBoundingClientRect().height) || 0; total += Math.max(0, Math.round(h)); });
|
||||
} catch(_) {}
|
||||
return total;
|
||||
}
|
||||
function installStickyPolyfill(tbl){
|
||||
if (!tbl || tbl.dataset.stickyPolyfill === '1') return;
|
||||
var thead = tbl.tHead; if (!thead) return;
|
||||
var cloneTable = document.createElement('table');
|
||||
cloneTable.className = tbl.className;
|
||||
cloneTable.style.position = 'fixed';
|
||||
cloneTable.style.top = getFixedHeaderOffset() + 'px';
|
||||
cloneTable.style.left = '0px';
|
||||
cloneTable.style.zIndex = '1045';
|
||||
cloneTable.style.display = 'none';
|
||||
cloneTable.style.background = 'var(--bs-light, #f8f9fa)';
|
||||
cloneTable.style.borderCollapse = 'separate';
|
||||
var cloneThead = thead.cloneNode(true);
|
||||
cloneTable.appendChild(cloneThead);
|
||||
document.body.appendChild(cloneTable);
|
||||
|
||||
function syncSizes(){
|
||||
var rect = tbl.getBoundingClientRect();
|
||||
var width = Math.round(rect.width);
|
||||
cloneTable.style.width = width + 'px';
|
||||
var origThs = thead.querySelectorAll('tr:first-child th');
|
||||
var cloneThs = cloneThead.querySelectorAll('tr:first-child th');
|
||||
for (var i=0; i<cloneThs.length; i++){
|
||||
var w = (origThs[i] && origThs[i].getBoundingClientRect().width) || 0;
|
||||
cloneThs[i].style.width = Math.round(w) + 'px';
|
||||
}
|
||||
}
|
||||
function update(){
|
||||
var rect = tbl.getBoundingClientRect();
|
||||
var off = getFixedHeaderOffset();
|
||||
var headH = (thead.getBoundingClientRect && thead.getBoundingClientRect().height) || 40;
|
||||
if (rect.top < off && rect.bottom > off + headH){
|
||||
syncSizes();
|
||||
cloneTable.style.left = Math.round(rect.left) + 'px';
|
||||
cloneTable.style.top = off + 'px';
|
||||
cloneTable.style.display = '';
|
||||
} else {
|
||||
cloneTable.style.display = 'none';
|
||||
}
|
||||
}
|
||||
window.addEventListener('scroll', update, { passive: true });
|
||||
window.addEventListener('resize', function(){ syncSizes(); update(); });
|
||||
setTimeout(function(){ syncSizes(); update(); }, 300);
|
||||
tbl.dataset.stickyPolyfill = '1';
|
||||
}
|
||||
document.addEventListener('DOMContentLoaded', function(){
|
||||
var tbl = document.getElementById('homeworkTrackTable');
|
||||
if (tbl) installStickyPolyfill(tbl);
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
<div class="d-flex justify-content-end mt-2 gap-2">
|
||||
<div class="btn-group" role="group" aria-label="Pagination bottom">
|
||||
<a class="btn btn-outline-secondary <?= $page <= 1 ? 'disabled' : '' ?>" href="<?= $mkHref(1) ?>">First</a>
|
||||
<a class="btn btn-outline-secondary <?= $page <= 1 ? 'disabled' : '' ?>" href="<?= $mkHref(max(1, $page-1)) ?>">Prev</a>
|
||||
<span class="btn btn-outline-secondary disabled">Page <?= $page ?> / <?= $totalPages ?></span>
|
||||
<a class="btn btn-outline-secondary <?= $page >= $totalPages ? 'disabled' : '' ?>" href="<?= $mkHref(min($totalPages, $page+1)) ?>">Next</a>
|
||||
<a class="btn btn-outline-secondary <?= $page >= $totalPages ? 'disabled' : '' ?>" href="<?= $mkHref($totalPages) ?>">Last</a>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="mt-2 small text-muted">Dates are Sundays within the current school term. Yellow indicates a No School day; light blue indicates a future Sunday.</div>
|
||||
<div class="small text-muted">Homework indices are mapped in order to non-event Sundays.</div>
|
||||
</div>
|
||||
|
||||
<?= $this->endSection() ?>
|
||||
@@ -0,0 +1,147 @@
|
||||
<?= $this->extend('layout/management_layout') ?>
|
||||
<?= $this->section('content') ?>
|
||||
<?php
|
||||
$scoresLocked = !empty($scoresLocked);
|
||||
$lockAttr = $scoresLocked ? 'disabled' : '';
|
||||
?>
|
||||
<div class="container-xxl py-5">
|
||||
<div class="container">
|
||||
<div class="text-center mx-auto mb-5 wow fadeInUp" data-wow-delay="0.1s" style="max-width: 600px;">
|
||||
<br>
|
||||
<h2 class="text-dark" style="font-family: Arial, sans-serif;">Midterm Exam Scores</h2>
|
||||
</div>
|
||||
|
||||
<?php if (session()->get('status')): ?>
|
||||
<div class="alert alert-success">
|
||||
<?= session()->get('status') ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php if ($scoresLocked): ?>
|
||||
<div class="alert alert-warning">
|
||||
Scores are locked for this class. Unlock to edit.
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<div class="d-flex justify-content-between mt-4 flex-wrap gap-2">
|
||||
<button type="submit" class="btn btn-success" <?= $lockAttr ?>>
|
||||
<i class="bi bi-save"></i> Save Changes
|
||||
</button>
|
||||
<a href="<?= base_url('/grading') ?>" class="btn btn-secondary">
|
||||
<i class="bi bi-x-circle"></i> Back to Scores
|
||||
</a>
|
||||
</div>
|
||||
<form action="<?= base_url('grading/updateMidtermExamScores') ?>" method="post"
|
||||
onsubmit="return validateForm()">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="semester" value="<?= esc($semester) ?>">
|
||||
<input type="hidden" name="school_year" value="<?= esc($schoolYear) ?>">
|
||||
<table id="midtermTable" class="table table-bordered mt-4 w-100" data-no-mgmt-sticky>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>School ID</th>
|
||||
<th>First Name</th>
|
||||
<th>Last Name</th>
|
||||
<th class="text-center">Midterm Exam Score</th>
|
||||
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($students as $index => $student): ?>
|
||||
<tr>
|
||||
<td><?= $index + 1 ?></td>
|
||||
<td><?= esc($student['school_id']) ?></td>
|
||||
<td>
|
||||
<a href="#" class="text-decoration-none" data-family-student-id="<?= (int)($student['student_id'] ?? 0) ?>">
|
||||
<?= esc($student['firstname']) ?>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="#" class="text-decoration-none" data-family-student-id="<?= (int)($student['student_id'] ?? 0) ?>">
|
||||
<?= esc($student['lastname']) ?>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<input type="number" name="final_score[<?= esc($student['student_id']) ?>][score]"
|
||||
value="<?= esc($student['score'] ?? '') ?>" class="form-control text-center" min="0" max="100" step="0.01"
|
||||
placeholder="Enter score (optional)" <?= $lockAttr ?>>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
|
||||
</table>
|
||||
|
||||
<div class="d-flex justify-content-between mt-4 flex-wrap gap-2">
|
||||
<button type="submit" class="btn btn-success" <?= $lockAttr ?>>
|
||||
<i class="bi bi-save"></i> Save Changes
|
||||
</button>
|
||||
<a href="<?= base_url('/grading') ?>" class="btn btn-secondary">
|
||||
<i class="bi bi-x-circle"></i> Back to Scores
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<br>
|
||||
</div>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
<?= $this->section('scripts') ?>
|
||||
<script>
|
||||
(function ($) {
|
||||
// Numeric ordering plug-in reading from input values
|
||||
if ($ && $.fn && $.fn.dataTable && !$.fn.dataTable.ext.order['dom-num-input']) {
|
||||
$.fn.dataTable.ext.order['dom-num-input'] = function (settings, col) {
|
||||
return this.api()
|
||||
.column(col, { order: 'index' })
|
||||
.nodes()
|
||||
.map(function (td) {
|
||||
const val = $('input', td).val();
|
||||
const num = parseFloat(val);
|
||||
return Number.isFinite(num) ? num : -Infinity;
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function initMidtermTable() {
|
||||
if (!($ && $.fn && $.fn.DataTable)) return;
|
||||
|
||||
const $table = $('#midtermTable');
|
||||
if (!$table.length) return;
|
||||
|
||||
// Sync data-order with live input values
|
||||
$table.on('input', 'input[type="number"]', function () {
|
||||
const num = parseFloat(this.value);
|
||||
this.closest('td')?.setAttribute('data-order', Number.isFinite(num) ? num : -Infinity);
|
||||
});
|
||||
|
||||
// Only one score column: index 4 (0-based)
|
||||
const scoreCols = [4];
|
||||
|
||||
$table.DataTable({
|
||||
paging: false,
|
||||
info: false,
|
||||
searching: false,
|
||||
autoWidth: false,
|
||||
order: [[2, 'asc'], [3, 'asc']],
|
||||
columnDefs: [
|
||||
{ targets: 0, orderable: false, searchable: false },
|
||||
{ targets: 1, className: 'text-nowrap' },
|
||||
{ targets: scoreCols, orderDataType: 'dom-num-input', orderSequence: ['desc', 'asc'], type: 'num' },
|
||||
],
|
||||
fixedHeader: {
|
||||
header: true,
|
||||
headerOffset: typeof getFixedHeaderOffset === 'function' ? getFixedHeaderOffset() : 0,
|
||||
},
|
||||
drawCallback: function () {
|
||||
const api = this.api();
|
||||
api.column(0, { search: 'applied', order: 'applied' }).nodes().each(function (cell, i) {
|
||||
cell.textContent = i + 1;
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
$(document).ready(initMidtermTable);
|
||||
})(jQuery);
|
||||
</script>
|
||||
<?= $this->endSection() ?>
|
||||
@@ -0,0 +1,148 @@
|
||||
<?= $this->extend('layout/management_layout') ?>
|
||||
<?= $this->section('content') ?>
|
||||
<?php
|
||||
$scoresLocked = !empty($scoresLocked);
|
||||
$lockAttr = $scoresLocked ? 'disabled' : '';
|
||||
?>
|
||||
<div class="container-xxl py-5">
|
||||
<div class="container">
|
||||
<div class="text-center mx-auto mb-5 wow fadeInUp" data-wow-delay="0.1s" style="max-width: 600px;">
|
||||
<br>
|
||||
<h2 class="text-dark" style="font-family: Arial, sans-serif;">Participation Scores</h2>
|
||||
</div>
|
||||
|
||||
<?php if (session()->get('status')): ?>
|
||||
<div class="alert alert-success">
|
||||
<?= session()->get('status') ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php if ($scoresLocked): ?>
|
||||
<div class="alert alert-warning">
|
||||
Scores are locked for this class. Unlock to edit.
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<div class="d-flex justify-content-between mt-4 flex-wrap gap-2">
|
||||
<button type="submit" class="btn btn-success" <?= $lockAttr ?>>
|
||||
<i class="bi bi-save"></i> Save Changes
|
||||
</button>
|
||||
<a href="<?= base_url('/grading') ?>" class="btn btn-secondary">
|
||||
<i class="bi bi-x-circle"></i> Back to Scores
|
||||
</a>
|
||||
</div>
|
||||
<form action="<?= base_url('grading/updateParticipationScore') ?>" method="post"
|
||||
onsubmit="return validateForm()">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="semester" value="<?= esc($semester) ?>">
|
||||
<input type="hidden" name="school_year" value="<?= esc($schoolYear) ?>">
|
||||
<table id="participationTable" class="table table-bordered mt-4 w-100" data-no-mgmt-sticky>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>School ID</th>
|
||||
<th>First Name</th>
|
||||
<th>Last Name</th>
|
||||
<th class="text-center">Participation Score</th>
|
||||
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($students as $index => $student): ?>
|
||||
<tr>
|
||||
<td><?= $index + 1 ?></td>
|
||||
<td><?= esc($student['school_id']) ?></td>
|
||||
<td>
|
||||
<a href="#" class="text-decoration-none" data-family-student-id="<?= (int)($student['student_id'] ?? 0) ?>">
|
||||
<?= esc($student['firstname']) ?>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="#" class="text-decoration-none" data-family-student-id="<?= (int)($student['student_id'] ?? 0) ?>">
|
||||
<?= esc($student['lastname']) ?>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<input type="number" name="final_score[<?= esc($student['student_id']) ?>][score]"
|
||||
value="<?= esc($student['score'] ?? '') ?>" class="form-control text-center" min="0" max="100" step="0.01"
|
||||
placeholder="Enter score (optional)" <?= $lockAttr ?>>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
|
||||
</table>
|
||||
|
||||
<div class="d-flex justify-content-between mt-4 flex-wrap gap-2">
|
||||
<button type="submit" class="btn btn-success" <?= $lockAttr ?>>
|
||||
<i class="bi bi-save"></i> Save Changes
|
||||
</button>
|
||||
<a href="<?= base_url('/grading') ?>" class="btn btn-secondary">
|
||||
<i class="bi bi-x-circle"></i> Back to Scores
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<br>
|
||||
</div>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
<?= $this->section('scripts') ?>
|
||||
<script>
|
||||
(function ($) {
|
||||
// Numeric ordering plug-in that reads the value inside an input
|
||||
if ($ && $.fn && $.fn.dataTable && !$.fn.dataTable.ext.order['dom-num-input']) {
|
||||
$.fn.dataTable.ext.order['dom-num-input'] = function (settings, col) {
|
||||
return this.api()
|
||||
.column(col, { order: 'index' })
|
||||
.nodes()
|
||||
.map(function (td) {
|
||||
const val = $('input', td).val();
|
||||
const num = parseFloat(val);
|
||||
return Number.isFinite(num) ? num : -Infinity;
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function initParticipationTable() {
|
||||
if (!($ && $.fn && $.fn.DataTable)) return;
|
||||
|
||||
const $table = $('#participationTable');
|
||||
if (!$table.length) return;
|
||||
|
||||
// Keep data-order synced with live input value
|
||||
$table.on('input', 'input[type="number"]', function () {
|
||||
const num = parseFloat(this.value);
|
||||
this.closest('td')?.setAttribute('data-order', Number.isFinite(num) ? num : -Infinity);
|
||||
});
|
||||
|
||||
const totalCols = $table.find('thead th').length;
|
||||
const scoreCols = [];
|
||||
for (let i = 4; i < totalCols; i++) scoreCols.push(i);
|
||||
|
||||
$table.DataTable({
|
||||
paging: false,
|
||||
info: false,
|
||||
searching: false,
|
||||
autoWidth: false,
|
||||
order: [[2, 'asc'], [3, 'asc']],
|
||||
columnDefs: [
|
||||
{ targets: 0, orderable: false, searchable: false },
|
||||
{ targets: 1, className: 'text-nowrap' },
|
||||
...(scoreCols.length ? [{ targets: scoreCols, orderDataType: 'dom-num-input', orderSequence: ['desc', 'asc'], type: 'num' }] : []),
|
||||
],
|
||||
fixedHeader: {
|
||||
header: true,
|
||||
headerOffset: typeof getFixedHeaderOffset === 'function' ? getFixedHeaderOffset() : 0,
|
||||
},
|
||||
drawCallback: function () {
|
||||
const api = this.api();
|
||||
api.column(0, { search: 'applied', order: 'applied' }).nodes().each(function (cell, i) {
|
||||
cell.textContent = i + 1;
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
$(document).ready(initParticipationTable);
|
||||
})(jQuery);
|
||||
</script>
|
||||
<?= $this->endSection() ?>
|
||||
@@ -0,0 +1,88 @@
|
||||
<?= $this->extend('layout/management_layout') ?>
|
||||
<?= $this->section('content') ?>
|
||||
<?php
|
||||
$students = $students ?? [];
|
||||
$levels = $levels ?? [];
|
||||
$classSectionId = $classSectionId ?? 0;
|
||||
$classSectionName = $classSectionName ?? '';
|
||||
$classId = (int) ($classId ?? 0);
|
||||
$schoolYear = $schoolYear ?? '';
|
||||
?>
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="d-flex flex-wrap align-items-center justify-content-between mb-3 gap-2">
|
||||
<div>
|
||||
<h2 class="mb-1">Placement Scores</h2>
|
||||
<p class="text-muted mb-0">
|
||||
Section: <?= esc($classSectionName ?: $classSectionId) ?> • School year: <?= esc($schoolYear) ?>
|
||||
</p>
|
||||
</div>
|
||||
<a href="<?= base_url('grading?class_id=' . rawurlencode((string) $classId)) ?>" class="btn btn-outline-secondary btn-sm">Back to grading</a>
|
||||
</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; ?>
|
||||
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header bg-white d-flex align-items-center justify-content-between">
|
||||
<h5 class="mb-0">Set placement score</h5>
|
||||
<span class="text-muted small"><?= count($students) ?> student<?= count($students) === 1 ? '' : 's' ?></span>
|
||||
</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: ?>
|
||||
<form action="<?= base_url('grading/placement') ?>" method="post">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="class_section_id" value="<?= esc($classSectionId) ?>">
|
||||
<input type="hidden" name="school_year" value="<?= esc($schoolYear) ?>">
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered table-striped align-middle mt-2">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>School ID</th>
|
||||
<th>First Name</th>
|
||||
<th>Last Name</th>
|
||||
<th style="width: 160px;">Placement Score</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($students as $student): ?>
|
||||
<?php
|
||||
$sid = (int) ($student['student_id'] ?? 0);
|
||||
$current = (string) ($levels[$sid] ?? '');
|
||||
?>
|
||||
<tr>
|
||||
<td><?= esc($student['school_id'] ?? '') ?></td>
|
||||
<td><?= esc($student['firstname'] ?? '') ?></td>
|
||||
<td><?= esc($student['lastname'] ?? '') ?></td>
|
||||
<td>
|
||||
<input type="number"
|
||||
name="placement_level[<?= esc((string) $sid) ?>]"
|
||||
class="form-control form-control-sm"
|
||||
min="0"
|
||||
max="100"
|
||||
step="1"
|
||||
value="<?= esc($current) ?>"
|
||||
placeholder="0-100">
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="d-flex justify-content-end">
|
||||
<button type="submit" class="btn btn-primary">Save placement scores</button>
|
||||
</div>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?= $this->endSection() ?>
|
||||
@@ -0,0 +1,91 @@
|
||||
<?= $this->extend('layout/management_layout') ?>
|
||||
<?= $this->section('content') ?>
|
||||
<?php
|
||||
$batch = $batch ?? [];
|
||||
$students = $students ?? [];
|
||||
$scores = $scores ?? [];
|
||||
$label = ucfirst((string) ($batch['placement_test'] ?? ''));
|
||||
$schoolYear = (string) ($batch['school_year'] ?? '');
|
||||
?>
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="d-flex flex-wrap align-items-center justify-content-between mb-3 gap-2">
|
||||
<div>
|
||||
<h2 class="mb-1">Edit Placement Batch</h2>
|
||||
<p class="text-muted mb-0">
|
||||
<?= esc($label) ?> • <?= esc($schoolYear) ?>
|
||||
</p>
|
||||
</div>
|
||||
<a href="<?= base_url('grading/placement') ?>" class="btn btn-outline-secondary btn-sm">Back to batches</a>
|
||||
</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; ?>
|
||||
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header bg-white d-flex align-items-center justify-content-between">
|
||||
<h5 class="mb-0">Students in batch</h5>
|
||||
<span class="text-muted small"><?= count($students) ?> student<?= count($students) === 1 ? '' : 's' ?></span>
|
||||
</div>
|
||||
<div class="card-body pt-2">
|
||||
<?php if (empty($students)): ?>
|
||||
<div class="alert alert-light border text-center mb-0">No active students found.</div>
|
||||
<?php else: ?>
|
||||
<form method="post" action="<?= base_url('grading/placement/batch/' . (int) ($batch['id'] ?? 0)) ?>">
|
||||
<?= csrf_field() ?>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered table-striped align-middle mt-2">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>School ID</th>
|
||||
<th>First Name</th>
|
||||
<th>Last Name</th>
|
||||
<th>Class Section</th>
|
||||
<th style="width: 160px;">Placement Score</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($students as $student): ?>
|
||||
<?php
|
||||
$sid = (int) ($student['student_id'] ?? 0);
|
||||
$scoreRow = $scores[$sid] ?? null;
|
||||
$current = $scoreRow ? (string) ($scoreRow['score'] ?? '') : '';
|
||||
$sectionName = (string) ($student['class_section_name'] ?? '');
|
||||
$className = (string) ($student['class_name'] ?? '');
|
||||
$sectionLabel = $className !== '' ? ($className . ' — ' . $sectionName) : $sectionName;
|
||||
?>
|
||||
<tr>
|
||||
<td><?= esc($student['school_id'] ?? '') ?></td>
|
||||
<td><?= esc($student['firstname'] ?? '') ?></td>
|
||||
<td><?= esc($student['lastname'] ?? '') ?></td>
|
||||
<td><?= esc($sectionLabel ?: '—') ?></td>
|
||||
<td>
|
||||
<input type="number"
|
||||
name="placement_level[<?= esc((string) $sid) ?>]"
|
||||
class="form-control form-control-sm"
|
||||
min="0"
|
||||
max="100"
|
||||
step="1"
|
||||
value="<?= esc($current) ?>"
|
||||
placeholder="0-100">
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="d-flex justify-content-end gap-2">
|
||||
<a href="<?= base_url('grading/placement') ?>" class="btn btn-outline-secondary">Cancel</a>
|
||||
<button type="submit" class="btn btn-primary">Save changes</button>
|
||||
</div>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?= $this->endSection() ?>
|
||||
@@ -0,0 +1,253 @@
|
||||
<?= $this->extend('layout/management_layout') ?>
|
||||
<?= $this->section('content') ?>
|
||||
<?php
|
||||
$schoolYear = $schoolYear ?? '';
|
||||
$students = $students ?? [];
|
||||
$batches = $batches ?? [];
|
||||
$batchDetails = $batchDetails ?? [];
|
||||
$placementTest = $placementTest ?? '';
|
||||
$showStudents = $showStudents ?? false;
|
||||
?>
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="d-flex flex-wrap align-items-center justify-content-between mb-3 gap-2">
|
||||
<div>
|
||||
<h2 class="mb-1">Placement Scores</h2>
|
||||
<p class="text-muted mb-0">Select a placement test, enter scores, and save as a batch.</p>
|
||||
</div>
|
||||
<a href="<?= base_url('grading') ?>" class="btn btn-outline-secondary btn-sm">Back to grading</a>
|
||||
</div>
|
||||
|
||||
<div class="card shadow-sm mb-4">
|
||||
<div class="card-header bg-white">
|
||||
<h5 class="mb-0">Choose placement test</h5>
|
||||
</div>
|
||||
<div class="card-body pt-2">
|
||||
<form method="get" action="<?= base_url('grading/placement') ?>" class="row g-3 align-items-end">
|
||||
<input type="hidden" name="open" value="1">
|
||||
<div class="col-12 col-md-6 col-lg-5">
|
||||
<label class="form-label">Choose placement test</label>
|
||||
<select name="placement_test" class="form-select" required>
|
||||
<option value="">Select a placement test</option>
|
||||
<option value="islamic" <?= $placementTest === 'islamic' ? 'selected' : '' ?>>Islamic Studies</option>
|
||||
<option value="quran" <?= $placementTest === 'quran' ? 'selected' : '' ?>>Quran</option>
|
||||
<option value="arabic" <?= $placementTest === 'arabic' ? 'selected' : '' ?>>Arabic</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-12 col-md-4 col-lg-3">
|
||||
<label class="form-label">School Year</label>
|
||||
<input type="text" name="school_year" class="form-control" value="<?= esc($schoolYear) ?>" placeholder="YYYY-YYYY" required>
|
||||
</div>
|
||||
<div class="col-12 col-md-2 col-lg-2 d-flex gap-2">
|
||||
<button type="submit" class="btn btn-primary w-100">Set Placement Scores</button>
|
||||
<a href="<?= base_url('grading/placement') ?>" class="btn btn-outline-secondary">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<?php if ($showStudents && !empty($students)): ?>
|
||||
<form method="post" action="<?= base_url('grading/placement-all') ?>" class="mt-3">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="placement_test" value="<?= esc($placementTest) ?>">
|
||||
<input type="hidden" name="school_year" value="<?= esc($schoolYear) ?>">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered table-striped align-middle mt-2" data-sortable>
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th data-sort-type="number">School ID</th>
|
||||
<th data-sort-type="text">First Name</th>
|
||||
<th data-sort-type="text">Last Name</th>
|
||||
<th data-sort-type="text">Class Section</th>
|
||||
<th data-sort-type="number" style="width: 160px;">Placement Score</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($students as $student): ?>
|
||||
<?php
|
||||
$sid = (int) ($student['student_id'] ?? 0);
|
||||
$sectionName = (string) ($student['class_section_name'] ?? '');
|
||||
$className = (string) ($student['class_name'] ?? '');
|
||||
$sectionLabel = $className !== '' ? ($className . ' — ' . $sectionName) : $sectionName;
|
||||
?>
|
||||
<tr>
|
||||
<td><?= esc($student['school_id'] ?? '') ?></td>
|
||||
<td><?= esc($student['firstname'] ?? '') ?></td>
|
||||
<td><?= esc($student['lastname'] ?? '') ?></td>
|
||||
<td><?= esc($sectionLabel ?: '—') ?></td>
|
||||
<td>
|
||||
<input type="number"
|
||||
name="placement_level[<?= esc((string) $sid) ?>]"
|
||||
class="form-control form-control-sm"
|
||||
min="0"
|
||||
max="100"
|
||||
step="1"
|
||||
value=""
|
||||
placeholder="0-100">
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="d-flex justify-content-end gap-2">
|
||||
<a href="<?= base_url('grading/placement') ?>" class="btn btn-outline-secondary">Cancel</a>
|
||||
<button type="submit" class="btn btn-primary">Save batch</button>
|
||||
</div>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header bg-white d-flex align-items-center justify-content-between">
|
||||
<h5 class="mb-0">Placement batches</h5>
|
||||
<span class="text-muted small"><?= count($batches) ?> batch<?= count($batches) === 1 ? '' : 'es' ?></span>
|
||||
</div>
|
||||
<div class="card-body pt-2">
|
||||
<?php if (empty($batches)): ?>
|
||||
<div class="alert alert-light border text-center mb-0">No placement batches yet.</div>
|
||||
<?php else: ?>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered table-striped align-middle mt-2" data-no-mgmt-sticky>
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Placement Test</th>
|
||||
<th>School Year</th>
|
||||
<th>Created</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($batches as $batch): ?>
|
||||
<?php
|
||||
$label = ucfirst((string) ($batch['placement_test'] ?? ''));
|
||||
$createdAt = $batch['created_at'] ?? '';
|
||||
$createdLabel = $createdAt ? date('M d, Y H:i', strtotime($createdAt)) : '';
|
||||
$batchId = (int) ($batch['id'] ?? 0);
|
||||
$detailRows = $batchDetails[$batchId] ?? [];
|
||||
$collapseId = 'batch-' . $batchId;
|
||||
?>
|
||||
<tr>
|
||||
<td>
|
||||
<button class="btn btn-link p-0 text-decoration-none" type="button" data-bs-toggle="collapse" data-bs-target="#<?= esc($collapseId) ?>" aria-expanded="false" aria-controls="<?= esc($collapseId) ?>">
|
||||
<?= esc($label) ?><?= $createdLabel ? ' — ' . esc($createdLabel) : '' ?>
|
||||
</button>
|
||||
</td>
|
||||
<td><?= esc($batch['school_year'] ?? '') ?></td>
|
||||
<td><?= esc($createdLabel) ?></td>
|
||||
<td>
|
||||
<a href="<?= base_url('grading/placement/batch/' . (int) $batch['id']) ?>" class="btn btn-sm btn-outline-primary">Edit</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="collapse" id="<?= esc($collapseId) ?>">
|
||||
<td colspan="4">
|
||||
<?php if (empty($detailRows)): ?>
|
||||
<div class="text-muted">No students with scores in this batch.</div>
|
||||
<?php else: ?>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm table-bordered align-middle mb-0" data-no-mgmt-sticky>
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>School ID</th>
|
||||
<th>First Name</th>
|
||||
<th>Last Name</th>
|
||||
<th>Class Section</th>
|
||||
<th>Score</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($detailRows as $row): ?>
|
||||
<?php
|
||||
$sectionName = (string) ($row['class_section_name'] ?? '');
|
||||
$className = (string) ($row['class_name'] ?? '');
|
||||
$sectionLabel = $className !== '' ? ($className . ' — ' . $sectionName) : $sectionName;
|
||||
?>
|
||||
<tr>
|
||||
<td><?= esc($row['school_id'] ?? '') ?></td>
|
||||
<td><?= esc($row['firstname'] ?? '') ?></td>
|
||||
<td><?= esc($row['lastname'] ?? '') ?></td>
|
||||
<td><?= esc($sectionLabel ?: '—') ?></td>
|
||||
<td><?= esc($row['score'] ?? '') ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
<?= $this->section('scripts') ?>
|
||||
<script>
|
||||
(() => {
|
||||
const tables = document.querySelectorAll('table[data-sortable]');
|
||||
tables.forEach((table) => {
|
||||
const tbody = table.tBodies[0];
|
||||
const headers = table.tHead ? Array.from(table.tHead.querySelectorAll('th')) : [];
|
||||
if (!tbody || headers.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const getCellValue = (cell) => {
|
||||
const input = cell.querySelector('input');
|
||||
if (input) {
|
||||
return input.value.trim();
|
||||
}
|
||||
return cell.textContent.trim();
|
||||
};
|
||||
|
||||
const parseValue = (value, type) => {
|
||||
if (type === 'number') {
|
||||
const num = parseFloat(value.replace(/[^0-9.\-]/g, ''));
|
||||
return Number.isNaN(num) ? null : num;
|
||||
}
|
||||
return value.toLowerCase();
|
||||
};
|
||||
|
||||
headers.forEach((th, index) => {
|
||||
th.style.cursor = 'pointer';
|
||||
th.setAttribute('role', 'button');
|
||||
th.setAttribute('aria-sort', 'none');
|
||||
|
||||
th.addEventListener('click', () => {
|
||||
const sortType = th.dataset.sortType || 'text';
|
||||
const rows = Array.from(tbody.querySelectorAll('tr')).map((row, idx) => ({
|
||||
row,
|
||||
idx,
|
||||
value: parseValue(getCellValue(row.children[index] || row.cells[index]), sortType),
|
||||
}));
|
||||
|
||||
const current = th.getAttribute('aria-sort') || 'none';
|
||||
const nextDir = current === 'ascending' ? 'descending' : 'ascending';
|
||||
|
||||
headers.forEach((h) => h.setAttribute('aria-sort', 'none'));
|
||||
th.setAttribute('aria-sort', nextDir);
|
||||
|
||||
rows.sort((a, b) => {
|
||||
if (a.value === b.value) {
|
||||
return a.idx - b.idx;
|
||||
}
|
||||
if (a.value === null) return 1;
|
||||
if (b.value === null) return -1;
|
||||
if (a.value < b.value) return nextDir === 'ascending' ? -1 : 1;
|
||||
return nextDir === 'ascending' ? 1 : -1;
|
||||
});
|
||||
|
||||
const fragment = document.createDocumentFragment();
|
||||
rows.forEach(({ row }) => fragment.appendChild(row));
|
||||
tbody.appendChild(fragment);
|
||||
});
|
||||
});
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
<?= $this->endSection() ?>
|
||||
@@ -0,0 +1,165 @@
|
||||
<?= $this->extend('layout/management_layout') ?>
|
||||
<?= $this->section('content') ?>
|
||||
|
||||
<?php
|
||||
$title = 'Update Project Scores';
|
||||
$type = 'project';
|
||||
$scoresLocked = !empty($scoresLocked);
|
||||
$lockAttr = $scoresLocked ? 'disabled' : '';
|
||||
?>
|
||||
|
||||
<div class="container-xxl py-5">
|
||||
<div class="container">
|
||||
<div class="text-center mx-auto mb-4 mt-3" style="max-width: 600px;">
|
||||
<h2 class="text-dark" style="font-family: Arial, sans-serif;"><?= esc($title) ?></h2>
|
||||
</div>
|
||||
|
||||
<?php if (session()->get('status')): ?>
|
||||
<div class="alert alert-success">
|
||||
<?= session()->get('status') ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php if ($scoresLocked): ?>
|
||||
<div class="alert alert-warning">
|
||||
Scores are locked for this class. Unlock to edit.
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<div class="d-flex justify-content-between mt-4 flex-wrap gap-2">
|
||||
<button type="submit" class="btn btn-success" <?= $lockAttr ?>>
|
||||
<i class="bi bi-save"></i> Save Changes
|
||||
</button>
|
||||
<a href="<?= base_url('/grading') ?>" class="btn btn-secondary">
|
||||
<i class="bi bi-x-circle"></i> Back to Scores
|
||||
</a>
|
||||
</div>
|
||||
<form action="<?= base_url('/grading/updateProject') ?>" method="post">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="semester" value="<?= esc($semester) ?>">
|
||||
<input type="hidden" name="school_year" value="<?= esc($schoolYear) ?>">
|
||||
<input type="hidden" name="class_section_id" value="<?= esc($classSectionId ?? '') ?>">
|
||||
|
||||
<table id="projectTable" class="table table-bordered mt-4 w-100" data-no-mgmt-sticky>
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>School ID</th>
|
||||
<th>First Name</th>
|
||||
<th>Last Name</th>
|
||||
<?php foreach ($projectHeaders as $index): ?>
|
||||
<th class="text-center"><?= esc("Project " . $index) ?></th>
|
||||
<?php endforeach; ?>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php $row = 1; ?>
|
||||
<?php foreach ($students as $student): ?>
|
||||
<?php
|
||||
$studentId = $student['id'];
|
||||
$scores = $projectScores[$studentId]['scores'] ?? [];
|
||||
?>
|
||||
<tr>
|
||||
<td><?= $row++ ?></td>
|
||||
<td><?= esc($student['school_id']) ?></td>
|
||||
<td>
|
||||
<a href="#" class="text-decoration-none" data-family-student-id="<?= (int)$studentId ?>">
|
||||
<?= esc($student['firstname']) ?>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="#" class="text-decoration-none" data-family-student-id="<?= (int)$studentId ?>">
|
||||
<?= esc($student['lastname']) ?>
|
||||
</a>
|
||||
</td>
|
||||
<input type="hidden" name="student_ids[]" value="<?= esc($studentId) ?>">
|
||||
|
||||
<?php foreach ($projectHeaders as $index): ?>
|
||||
<?php $scoreValue = $scores[$index] ?? ''; ?>
|
||||
<td class="<?= $scoreValue === '' ? 'bg-warning-subtle' : '' ?>">
|
||||
<input type="number"
|
||||
name="scores[<?= $studentId ?>][<?= $index ?>]"
|
||||
value="<?= esc(is_array($scoreValue) ? '' : $scoreValue) ?>"
|
||||
class="form-control text-center"
|
||||
min="0" max="100" step="0.01" <?= $lockAttr ?>>
|
||||
</td>
|
||||
<?php endforeach; ?>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
|
||||
</table>
|
||||
|
||||
<div class="d-flex justify-content-between mt-4 flex-wrap gap-2">
|
||||
<button type="submit" class="btn btn-success" <?= $lockAttr ?>>
|
||||
<i class="bi bi-save"></i> Save Changes
|
||||
</button>
|
||||
<a href="<?= base_url('/grading') ?>" class="btn btn-secondary">
|
||||
<i class="bi bi-x-circle"></i> Back to Scores
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
<?= $this->section('scripts') ?>
|
||||
<script>
|
||||
(function ($) {
|
||||
// Numeric ordering based on input values
|
||||
if ($ && $.fn && $.fn.dataTable && !$.fn.dataTable.ext.order['dom-num-input']) {
|
||||
$.fn.dataTable.ext.order['dom-num-input'] = function (settings, col) {
|
||||
return this.api()
|
||||
.column(col, { order: 'index' })
|
||||
.nodes()
|
||||
.map(function (td) {
|
||||
const val = $('input', td).val();
|
||||
const num = parseFloat(val);
|
||||
return Number.isFinite(num) ? num : -Infinity;
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function initProjectTable() {
|
||||
if (!($ && $.fn && $.fn.DataTable)) return;
|
||||
|
||||
const $table = $('#projectTable');
|
||||
if (!$table.length) return;
|
||||
|
||||
// Sync data-order with current input values
|
||||
$table.on('input', 'input[type="number"]', function () {
|
||||
const num = parseFloat(this.value);
|
||||
this.closest('td')?.setAttribute('data-order', Number.isFinite(num) ? num : -Infinity);
|
||||
});
|
||||
|
||||
const totalCols = $table.find('thead th').length;
|
||||
const scoreCols = [];
|
||||
for (let i = 4; i < totalCols; i++) scoreCols.push(i);
|
||||
|
||||
$table.DataTable({
|
||||
paging: false,
|
||||
info: false,
|
||||
searching: false,
|
||||
autoWidth: false,
|
||||
order: [[2, 'asc'], [3, 'asc']],
|
||||
columnDefs: [
|
||||
{ targets: 0, orderable: false, searchable: false },
|
||||
{ targets: 1, className: 'text-nowrap' },
|
||||
...(scoreCols.length ? [{ targets: scoreCols, orderDataType: 'dom-num-input', orderSequence: ['desc', 'asc'], type: 'num' }] : []),
|
||||
],
|
||||
fixedHeader: {
|
||||
header: true,
|
||||
headerOffset: typeof getFixedHeaderOffset === 'function' ? getFixedHeaderOffset() : 0,
|
||||
},
|
||||
drawCallback: function () {
|
||||
const api = this.api();
|
||||
api.column(0, { search: 'applied', order: 'applied' }).nodes().each(function (cell, i) {
|
||||
cell.textContent = i + 1;
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
$(document).ready(initProjectTable);
|
||||
})(jQuery);
|
||||
</script>
|
||||
<?= $this->endSection() ?>
|
||||
@@ -0,0 +1,166 @@
|
||||
<?= $this->extend('layout/management_layout') ?>
|
||||
<?= $this->section('content') ?>
|
||||
|
||||
<?php
|
||||
$title = 'Update Quiz Scores';
|
||||
$type = 'quiz';
|
||||
$scoresLocked = !empty($scoresLocked);
|
||||
$lockAttr = $scoresLocked ? 'disabled' : '';
|
||||
?>
|
||||
|
||||
<div class="container-xxl py-5">
|
||||
<div class="container">
|
||||
<div class="text-center mx-auto mb-4 mt-3" style="max-width: 600px;">
|
||||
<h2 class="text-dark" style="font-family: Arial, sans-serif;"><?= esc($title) ?></h2>
|
||||
</div>
|
||||
|
||||
<?php if (session()->get('status')): ?>
|
||||
<div class="alert alert-success">
|
||||
<?= session()->get('status') ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php if ($scoresLocked): ?>
|
||||
<div class="alert alert-warning">
|
||||
Scores are locked for this class. Unlock to edit.
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<div class="d-flex justify-content-between mt-4 flex-wrap gap-2">
|
||||
<button type="submit" class="btn btn-success" <?= $lockAttr ?>>
|
||||
<i class="bi bi-save"></i> Save Changes
|
||||
</button>
|
||||
<a href="<?= base_url('/grading') ?>" class="btn btn-secondary">
|
||||
<i class="bi bi-x-circle"></i> Back to Scores
|
||||
</a>
|
||||
</div>
|
||||
<form action="<?= base_url('/grading/updateQuiz') ?>" method="post">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="semester" value="<?= esc($semester) ?>">
|
||||
<input type="hidden" name="school_year" value="<?= esc($schoolYear) ?>">
|
||||
|
||||
<table id="quizTable" class="table table-bordered mt-4 w-100" data-no-mgmt-sticky>
|
||||
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>School ID</th>
|
||||
<th>First Name</th>
|
||||
<th>Last Name</th>
|
||||
<?php foreach ($quizHeaders as $index): ?>
|
||||
<th class="text-center"><?= esc("Quiz " . $index) ?></th>
|
||||
<?php endforeach; ?>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php $row = 1; ?>
|
||||
<?php foreach ($students as $student): ?>
|
||||
<?php
|
||||
$studentId = $student['id'];
|
||||
$scores = $quizScores[$studentId]['scores'] ?? [];
|
||||
?>
|
||||
<tr>
|
||||
<td><?= $row++ ?></td>
|
||||
<td><?= esc($student['school_id']) ?></td>
|
||||
<td>
|
||||
<a href="#" class="text-decoration-none" data-family-student-id="<?= (int)$studentId ?>">
|
||||
<?= esc($student['firstname']) ?>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="#" class="text-decoration-none" data-family-student-id="<?= (int)$studentId ?>">
|
||||
<?= esc($student['lastname']) ?>
|
||||
</a>
|
||||
</td>
|
||||
<input type="hidden" name="student_ids[]" value="<?= esc($studentId) ?>">
|
||||
|
||||
<?php foreach ($quizHeaders as $index): ?>
|
||||
<?php $scoreValue = $scores[$index] ?? ''; ?>
|
||||
<td class="<?= $scoreValue === '' ? 'bg-warning-subtle' : '' ?>">
|
||||
<input type="number"
|
||||
name="scores[<?= $studentId ?>][<?= $index ?>]"
|
||||
value="<?= esc(is_array($scoreValue) ? '' : $scoreValue) ?>"
|
||||
class="form-control text-center"
|
||||
min="0" max="100" step="0.01" <?= $lockAttr ?>>
|
||||
</td>
|
||||
<?php endforeach; ?>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
|
||||
</table>
|
||||
|
||||
<div class="d-flex justify-content-between mt-4 flex-wrap gap-2">
|
||||
<button type="submit" class="btn btn-success" <?= $lockAttr ?>>
|
||||
<i class="bi bi-save"></i> Save Changes
|
||||
</button>
|
||||
<a href="<?= base_url('/grading') ?>" class="btn btn-secondary">
|
||||
<i class="bi bi-x-circle"></i> Back to Scores
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
<?= $this->section('scripts') ?>
|
||||
<script>
|
||||
(function ($) {
|
||||
// Custom ordering plug-in: read numeric value from an input inside the cell
|
||||
if ($ && $.fn && $.fn.dataTable && !$.fn.dataTable.ext.order['dom-num-input']) {
|
||||
$.fn.dataTable.ext.order['dom-num-input'] = function (settings, col) {
|
||||
return this.api()
|
||||
.column(col, { order: 'index' })
|
||||
.nodes()
|
||||
.map(function (td) {
|
||||
const val = $('input', td).val();
|
||||
const num = parseFloat(val);
|
||||
return Number.isFinite(num) ? num : -Infinity;
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function initQuizTable() {
|
||||
if (!($ && $.fn && $.fn.DataTable)) return;
|
||||
|
||||
const $table = $('#quizTable');
|
||||
if (!$table.length) return;
|
||||
|
||||
// Keep data-order in sync with live input values
|
||||
$table.on('input', 'input[type="number"]', function () {
|
||||
const num = parseFloat(this.value);
|
||||
this.closest('td')?.setAttribute('data-order', Number.isFinite(num) ? num : -Infinity);
|
||||
});
|
||||
|
||||
// Score columns start after Last Name (index 0-based)
|
||||
const totalCols = $table.find('thead th').length;
|
||||
const scoreCols = [];
|
||||
for (let i = 4; i < totalCols; i++) scoreCols.push(i);
|
||||
|
||||
$table.DataTable({
|
||||
paging: false,
|
||||
info: false,
|
||||
searching: false,
|
||||
autoWidth: false,
|
||||
order: [[2, 'asc'], [3, 'asc']],
|
||||
columnDefs: [
|
||||
{ targets: 0, orderable: false, searchable: false },
|
||||
{ targets: 1, className: 'text-nowrap' },
|
||||
...(scoreCols.length ? [{ targets: scoreCols, orderDataType: 'dom-num-input', orderSequence: ['desc', 'asc'], type: 'num' }] : []),
|
||||
],
|
||||
fixedHeader: {
|
||||
header: true,
|
||||
headerOffset: typeof getFixedHeaderOffset === 'function' ? getFixedHeaderOffset() : 0,
|
||||
},
|
||||
drawCallback: function () {
|
||||
const api = this.api();
|
||||
api.column(0, { search: 'applied', order: 'applied' }).nodes().each(function (cell, i) {
|
||||
cell.textContent = i + 1;
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
$(document).ready(initQuizTable);
|
||||
})(jQuery);
|
||||
</script>
|
||||
<?= $this->endSection() ?>
|
||||
@@ -0,0 +1,64 @@
|
||||
<?= $this->extend('layout/management_layout') ?>
|
||||
<?= $this->section('content') ?>
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="wrapper">
|
||||
<h2 class="text-center mt-4 mb-3">Schedule Parent Meeting</h2>
|
||||
|
||||
<div class="card mx-auto" style="max-width: 720px;">
|
||||
<div class="card-body">
|
||||
<?php if (session()->getFlashdata('error')): ?>
|
||||
<div class="alert alert-danger"><?= esc(session()->getFlashdata('error')) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<form method="post" action="<?= site_url('grading/below-60/schedule') ?>">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="student_id" value="<?= esc((string)($studentId ?? 0)) ?>">
|
||||
<input type="hidden" name="semester" value="<?= esc((string)($semester ?? '')) ?>">
|
||||
<input type="hidden" name="school_year" value="<?= esc((string)($schoolYear ?? '')) ?>">
|
||||
|
||||
<div class="mb-2">
|
||||
<strong>Student:</strong> <?= esc($studentName ?? 'Student') ?>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<strong>Parent:</strong> <?= esc($parentName ?? 'Parent/Guardian') ?>
|
||||
</div>
|
||||
<?php if (!empty($classSection)): ?>
|
||||
<div class="mb-3">
|
||||
<strong>Section:</strong> <?= esc($classSection) ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Date</label>
|
||||
<input type="date" name="date" class="form-control" required>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Time</label>
|
||||
<input type="time" name="time" class="form-control">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3">
|
||||
<label class="form-label">Notes (optional)</label>
|
||||
<textarea name="notes" class="form-control" rows="4"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 d-flex justify-content-between">
|
||||
<?php
|
||||
$backQuery = http_build_query([
|
||||
'semester' => (string)($semester ?? ''),
|
||||
'school_year' => (string)($schoolYear ?? ''),
|
||||
]);
|
||||
?>
|
||||
<a class="btn btn-outline-secondary" href="<?= base_url('grading/below-60') . ($backQuery ? ('?' . $backQuery) : '') ?>">Back</a>
|
||||
<button type="submit" class="btn btn-primary">Create Meeting</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?= $this->endSection() ?>
|
||||
@@ -0,0 +1,112 @@
|
||||
<?php
|
||||
// views/grading/homework.php, quiz.php, project.php, test.php, midterm.php, final.php, comments.php
|
||||
// Use this as a shared structure but each type will set its own unique variables below
|
||||
|
||||
// Set title based on the view file
|
||||
$viewFile = basename(__FILE__, '.php');
|
||||
|
||||
switch ($viewFile) {
|
||||
case 'homework':
|
||||
$type = 'homework';
|
||||
$title = 'Homework';
|
||||
break;
|
||||
case 'quiz':
|
||||
$type = 'quiz';
|
||||
$title = 'Quiz';
|
||||
break;
|
||||
case 'project':
|
||||
$type = 'project';
|
||||
$title = 'Project';
|
||||
break;
|
||||
case 'test':
|
||||
$type = 'test';
|
||||
$title = 'Test';
|
||||
break;
|
||||
case 'midterm':
|
||||
$type = 'midterm';
|
||||
$title = 'Midterm Exam';
|
||||
break;
|
||||
case 'final':
|
||||
$type = 'final';
|
||||
$title = 'Final Exam';
|
||||
break;
|
||||
case 'comments':
|
||||
$type = 'comments';
|
||||
$title = 'General Comments';
|
||||
break;
|
||||
default:
|
||||
$type = 'homework';
|
||||
$title = 'Homework';
|
||||
}
|
||||
|
||||
$scoresLocked = !empty($scoresLocked);
|
||||
$lockAttr = $scoresLocked ? 'disabled' : '';
|
||||
?>
|
||||
|
||||
<?= $this->extend('layout/management_layout') ?>
|
||||
<?= $this->section('content') ?>
|
||||
<div class="container mt-5">
|
||||
<h2 class="text-center mb-4">
|
||||
<?= esc($title) ?> Scores for
|
||||
<a href="#" class="text-decoration-none" data-family-student-id="<?= (int)($student['id'] ?? 0) ?>">
|
||||
<?= esc($student['firstname'] . ' ' . $student['lastname']) ?>
|
||||
</a>
|
||||
</h2>
|
||||
<?php if ($scoresLocked): ?>
|
||||
<div class="alert alert-warning text-center">
|
||||
Scores are locked for this class. Unlock to edit.
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<form action="<?= base_url('grading/update') ?>" method="post">
|
||||
<input type="hidden" name="type" value="<?= esc($type) ?>">
|
||||
<input type="hidden" name="student_id" value="<?= esc($student['id']) ?>">
|
||||
<input type="hidden" name="class_section_id" value="<?= esc($classSectionId) ?>">
|
||||
|
||||
|
||||
<?php if (in_array($type, ['homework', 'quiz', 'project'])): ?>
|
||||
<table class="table table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Score</th>
|
||||
<th>Comment</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($scores as $index => $row): ?>
|
||||
<tr>
|
||||
<td><?= $index + 1 ?></td>
|
||||
<td>
|
||||
<input type="hidden" name="score_ids[]" value="<?= $row['id'] ?>">
|
||||
<input type="number" class="form-control" name="scores[]" value="<?= $row['score'] ?>" <?= $lockAttr ?>>
|
||||
</td>
|
||||
<td>
|
||||
<textarea class="form-control" name="comments[]" <?= $lockAttr ?>><?= esc($row['comment'] ?? '') ?></textarea>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php elseif (in_array($type, ['midterm', 'final', 'test'])): ?>
|
||||
<div class="form-group">
|
||||
<label>Score</label>
|
||||
<input type="number" class="form-control" name="score" value="<?= $scores[0]['score'] ?? '' ?>" <?= $lockAttr ?>>
|
||||
</div>
|
||||
<?php elseif ($type === 'comments'): ?>
|
||||
<div class="form-group">
|
||||
<label>General Comment</label>
|
||||
<textarea class="form-control" name="comment" rows="5" <?= $lockAttr ?>><?= $scores[0]['comment'] ?? '' ?></textarea>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="form-group text-center mt-3">
|
||||
<button type="submit" class="btn btn-primary" <?= $lockAttr ?>>Save Changes</button>
|
||||
</div>
|
||||
</form>
|
||||
<div class="text-center mt-4">
|
||||
<a href="<?= base_url('grading') ?>" class="btn btn-secondary">Return to Main Page</a>
|
||||
</div>
|
||||
<br>
|
||||
</div>
|
||||
<?= $this->endSection() ?>
|
||||
Reference in New Issue
Block a user