recreate project

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