316 lines
14 KiB
PHP
Executable File
316 lines
14 KiB
PHP
Executable File
<?= $this->extend('layout/main_layout') ?>
|
||
<?= $this->section('content') ?>
|
||
<div class="container-fluid py-5">
|
||
<div class="container-fluid">
|
||
<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;">Update Quiz Scores</h2>
|
||
</div>
|
||
|
||
<?php if (session()->get('status')): ?>
|
||
<div class="alert alert-success">
|
||
<?= session()->get('status') ?>
|
||
</div>
|
||
<?php endif; ?>
|
||
<?php
|
||
$formatScore = static function ($value) {
|
||
return ($value === null || $value === '') ? '' : esc($value);
|
||
};
|
||
$missingOkMap = $missingOkMap ?? [];
|
||
?>
|
||
<form id="quizForm" action="<?= base_url('/teacher/updateQuizScores') ?>" method="post">
|
||
<?= csrf_field() ?>
|
||
<input type="hidden" name="semester" value="<?= esc($semester) ?>">
|
||
<input type="hidden" name="school_year" value="<?= esc($schoolYear) ?>">
|
||
<div class="table-responsive">
|
||
<table id="quizTable" class="table table-bordered mt-4 w-100">
|
||
<thead>
|
||
<tr>
|
||
<th>#</th>
|
||
<th>Student Name</th>
|
||
<?php foreach ($quizHeaders as $quizLabel): ?>
|
||
<th class="text-center" data-index="<?= esc($quizLabel) ?>"><?= esc("Quiz " . $quizLabel) ?></th>
|
||
<?php endforeach; ?>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<?php foreach ($students as $index => $student): ?>
|
||
<tr>
|
||
<td><?= $index + 1 ?></td>
|
||
<td>
|
||
<?= esc($student['firstname']) ?> <?= esc($student['lastname']) ?>
|
||
<input type="hidden" class="student-id" value="<?= $student['student_id'] ?>">
|
||
</td>
|
||
<?php foreach ($quizHeaders as $quizLabel): ?>
|
||
<?php
|
||
$rawScore = $student['scores'][$quizLabel] ?? null;
|
||
$isEmptyScore = ($rawScore === null || $rawScore === '');
|
||
$studentId = (int) ($student['student_id'] ?? 0);
|
||
$isMissingOk = !empty($missingOkMap[$studentId][$quizLabel]);
|
||
?>
|
||
<td>
|
||
<input type="number"
|
||
name="scores[<?= intval($student['student_id']) ?>][<?= intval($quizLabel) ?>]"
|
||
value="<?= $formatScore($rawScore) ?>"
|
||
class="form-control text-center <?= $isEmptyScore ? 'score-empty' : '' ?>"
|
||
min="0" max="100" step="0.01">
|
||
<label class="missing-check mt-1 <?= $isEmptyScore ? '' : 'd-none' ?>">
|
||
<input type="checkbox"
|
||
name="missing_ok[<?= intval($student['student_id']) ?>][<?= intval($quizLabel) ?>]"
|
||
value="1"
|
||
class="missing-score-checkbox"
|
||
data-field-label="Quiz <?= esc($quizLabel) ?>" <?= $isMissingOk ? 'checked' : '' ?>>
|
||
Missing ok
|
||
</label>
|
||
</td>
|
||
<?php endforeach; ?>
|
||
</tr>
|
||
<?php endforeach; ?>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<div class="d-flex justify-content-between mt-3">
|
||
<button type="submit" class="btn btn-success">Save Changes</button>
|
||
<button type="button" id="addColumnBtn" class="btn btn-warning">Add New Column</button>
|
||
<button type="button" id="removeColumnBtn" class="btn btn-danger">Remove Empty Column</button>
|
||
<a href="<?= base_url('/teacher/scores'); ?>" class="btn btn-info">
|
||
<i class="bi bi-arrow-left-circle"></i> Back to Scores
|
||
</a>
|
||
</div>
|
||
</form>
|
||
<br>
|
||
</div>
|
||
<?= $this->endSection() ?>
|
||
|
||
<?= $this->section('scripts') ?>
|
||
<link rel="stylesheet" href="https://cdn.datatables.net/1.13.10/css/jquery.dataTables.min.css">
|
||
<link rel="stylesheet" href="https://cdn.datatables.net/1.13.10/css/dataTables.bootstrap5.min.css">
|
||
<script src="https://cdn.datatables.net/1.13.10/js/jquery.dataTables.min.js"></script>
|
||
<script src="https://cdn.datatables.net/1.13.10/js/dataTables.bootstrap5.min.js"></script>
|
||
<script>
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
const $table = window.jQuery ? window.jQuery('#quizTable') : null;
|
||
|
||
// Numeric ordering based on input values
|
||
if (window.jQuery && jQuery.fn && jQuery.fn.dataTable && !jQuery.fn.dataTable.ext.order['dom-num-input']) {
|
||
jQuery.fn.dataTable.ext.order['dom-num-input'] = function (settings, col) {
|
||
return this.api()
|
||
.column(col, { order: 'index' })
|
||
.nodes()
|
||
.map(function (td) {
|
||
const val = jQuery('input', td).val();
|
||
const num = parseFloat(val);
|
||
return Number.isFinite(num) ? num : -Infinity;
|
||
});
|
||
};
|
||
}
|
||
|
||
function syncHeaderTitles() {
|
||
document.querySelectorAll('thead th[data-index]').forEach((th) => {
|
||
const idx = th.dataset.index;
|
||
if (!idx) return;
|
||
const text = th.textContent.trim();
|
||
if (!/^Quiz\\s+\\d+$/i.test(text)) {
|
||
th.textContent = "Quiz " + idx;
|
||
}
|
||
});
|
||
}
|
||
|
||
function initQuizTable() {
|
||
if (!(window.jQuery && jQuery.fn && jQuery.fn.DataTable && $table && $table.length)) return;
|
||
|
||
const totalCols = $table.find('thead th').length;
|
||
const scoreCols = [];
|
||
for (let i = 2; i < totalCols; i++) scoreCols.push(i);
|
||
|
||
$table.DataTable({
|
||
paging: false,
|
||
info: false,
|
||
searching: false,
|
||
autoWidth: false,
|
||
order: [[1, 'asc']],
|
||
columnDefs: [
|
||
{ targets: 0, orderable: false, searchable: false },
|
||
...(scoreCols.length ? [{ targets: scoreCols, orderDataType: 'dom-num-input', orderSequence: ['desc', 'asc'], type: 'num' }] : []),
|
||
],
|
||
drawCallback: function () {
|
||
const api = this.api();
|
||
api.column(0, { search: 'applied', order: 'applied' }).nodes().each(function (cell, i) {
|
||
cell.textContent = i + 1;
|
||
});
|
||
},
|
||
});
|
||
syncHeaderTitles();
|
||
}
|
||
|
||
function refreshDataTable() {
|
||
if (!(window.jQuery && jQuery.fn && jQuery.fn.DataTable && $table && $table.length)) return;
|
||
if (jQuery.fn.DataTable.isDataTable($table)) {
|
||
$table.DataTable().destroy();
|
||
}
|
||
initQuizTable();
|
||
}
|
||
|
||
function destroyDataTable() {
|
||
if (!(window.jQuery && jQuery.fn && jQuery.fn.DataTable && $table && $table.length)) return;
|
||
if (jQuery.fn.DataTable.isDataTable($table)) {
|
||
$table.DataTable().destroy();
|
||
}
|
||
}
|
||
|
||
syncHeaderTitles();
|
||
initQuizTable();
|
||
const toggleEmptyClass = (input) => {
|
||
if (input.value === '' || input.value === null) {
|
||
input.classList.add('score-empty');
|
||
} else {
|
||
input.classList.remove('score-empty');
|
||
}
|
||
};
|
||
const toggleMissingCheck = (input) => {
|
||
const label = input.parentElement ? input.parentElement.querySelector('.missing-check') : null;
|
||
if (!label) return;
|
||
const empty = input.value === '' || input.value === null;
|
||
label.classList.toggle('d-none', !empty);
|
||
if (!empty) {
|
||
const checkbox = label.querySelector('input[type="checkbox"]');
|
||
if (checkbox) checkbox.checked = false;
|
||
}
|
||
};
|
||
|
||
const attachInputListeners = () => {
|
||
document.querySelectorAll('input[type=\"number\"]').forEach((input) => {
|
||
toggleEmptyClass(input);
|
||
toggleMissingCheck(input);
|
||
if (!input.dataset.listenerAttached) {
|
||
input.addEventListener('input', () => {
|
||
toggleEmptyClass(input);
|
||
toggleMissingCheck(input);
|
||
});
|
||
input.dataset.listenerAttached = 'true';
|
||
}
|
||
});
|
||
};
|
||
attachInputListeners();
|
||
|
||
// Initialize counter from current highest Quiz index
|
||
let quizCounter = getMaxQuizIndex() + 1;
|
||
|
||
function getMaxQuizIndex() {
|
||
const headers = document.querySelectorAll('thead th');
|
||
let maxIndex = 0;
|
||
headers.forEach(th => {
|
||
const dataIndex = th.dataset.index;
|
||
if (dataIndex && !isNaN(parseInt(dataIndex, 10))) {
|
||
maxIndex = Math.max(maxIndex, parseInt(dataIndex, 10));
|
||
return;
|
||
}
|
||
const text = th.textContent.trim();
|
||
const match = text.match(/^Quiz\s+(\d+)$/i);
|
||
if (match) {
|
||
const index = parseInt(match[1], 10);
|
||
if (!isNaN(index)) {
|
||
maxIndex = Math.max(maxIndex, index);
|
||
}
|
||
}
|
||
});
|
||
return maxIndex;
|
||
}
|
||
|
||
const addBtn = document.getElementById('addColumnBtn');
|
||
const removeBtn = document.getElementById('removeColumnBtn');
|
||
|
||
// ➕ Add Column
|
||
addBtn.addEventListener('click', function(e) {
|
||
e.preventDefault();
|
||
|
||
destroyDataTable();
|
||
const newIndex = quizCounter++;
|
||
|
||
// Add new header
|
||
const headerRow = document.querySelector('thead tr');
|
||
const newTh = document.createElement('th');
|
||
newTh.textContent = "Quiz " + newIndex;
|
||
newTh.classList.add(`quiz-col-${newIndex}`, 'quiz-header', 'dynamic-col', 'text-center');
|
||
newTh.dataset.index = newIndex;
|
||
headerRow.appendChild(newTh);
|
||
|
||
// Add column for each student row
|
||
const rows = document.querySelectorAll('tbody tr');
|
||
rows.forEach(row => {
|
||
const studentIdInput = row.querySelector('.student-id');
|
||
const studentId = studentIdInput ? studentIdInput.value : null;
|
||
|
||
if (!studentId) {
|
||
console.warn("Missing student ID input for row:", row);
|
||
const newTd = document.createElement('td');
|
||
newTd.classList.add(`quiz-col-${newIndex}`, 'dynamic-col');
|
||
row.appendChild(newTd);
|
||
return;
|
||
}
|
||
|
||
const newTd = document.createElement('td');
|
||
newTd.classList.add(`quiz-col-${newIndex}`, 'dynamic-col');
|
||
newTd.innerHTML = `
|
||
<input type=\"number\"
|
||
name=\"scores[${studentId}][${newIndex}]\"
|
||
class=\"form-control text-center score-empty\"
|
||
min=\"0\" max=\"100\" step=\"0.01\">
|
||
<label class=\"missing-check mt-1\">
|
||
<input type=\"checkbox\"
|
||
name=\"missing_ok[${studentId}][${newIndex}]\"
|
||
value=\"1\"
|
||
class=\"missing-score-checkbox\"
|
||
data-field-label=\"Quiz ${newIndex}\">
|
||
Missing ok
|
||
</label>`;
|
||
row.appendChild(newTd);
|
||
});
|
||
|
||
attachInputListeners();
|
||
initQuizTable();
|
||
});
|
||
|
||
// ❌ Remove Last Column
|
||
removeBtn.addEventListener('click', function(e) {
|
||
e.preventDefault();
|
||
|
||
destroyDataTable();
|
||
const dynamicHeaders = document.querySelectorAll('th.dynamic-col');
|
||
if (dynamicHeaders.length === 0) {
|
||
alert("No dynamic columns to remove.");
|
||
return;
|
||
}
|
||
|
||
const lastHeader = dynamicHeaders[dynamicHeaders.length - 1];
|
||
const index = lastHeader.dataset.index;
|
||
lastHeader.remove();
|
||
|
||
const rows = document.querySelectorAll('tbody tr');
|
||
rows.forEach(row => {
|
||
const cell = row.querySelector(`.quiz-col-${index}.dynamic-col`);
|
||
if (cell) {
|
||
cell.remove();
|
||
}
|
||
});
|
||
|
||
// Sync the counter with remaining columns
|
||
quizCounter = getMaxQuizIndex() + 1;
|
||
initQuizTable();
|
||
});
|
||
});
|
||
</script>
|
||
<style>
|
||
.score-empty {
|
||
background-color: #fff3cd;
|
||
}
|
||
.missing-check {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
font-size: 0.75rem;
|
||
color: #6c757d;
|
||
}
|
||
</style>
|
||
<?= $this->endSection() ?>
|