644 lines
32 KiB
PHP
644 lines
32 KiB
PHP
<?= $this->extend('layout/management_layout') ?>
|
|
<?= $this->section('content') ?>
|
|
<div class="container-fluid">
|
|
<div class="wrapper">
|
|
<div class="content">
|
|
<h2 class="text-center mt-4 mb-3">Student Class Assignment</h2>
|
|
<div class="row g-2 align-items-center justify-content-center mb-3">
|
|
<div class="col-auto"><label for="scaYearSelect" class="col-form-label">School year</label></div>
|
|
<div class="col-auto">
|
|
<form method="get" action="<?= site_url('administrator/student_class_assignment') ?>" class="d-flex align-items-center gap-2">
|
|
<select id="scaYearSelect" name="schoolYear" class="form-select form-select-sm" style="min-width: 180px;">
|
|
<?php
|
|
$years = isset($schoolYears) && is_array($schoolYears) ? $schoolYears : [];
|
|
if (empty($years) && !empty($selectedYear)) $years = [$selectedYear];
|
|
foreach ($years as $y): $val = is_array($y) && isset($y['school_year']) ? $y['school_year'] : (string)$y; ?>
|
|
<option value="<?= esc($val) ?>" <?= ((string)($selectedYear ?? '') === (string)$val) ? 'selected' : '' ?>>
|
|
<?= esc($val) ?>
|
|
</option>
|
|
<?php endforeach; ?>
|
|
</select>
|
|
<?php $semVal = (string)($semester ?? ($_GET['semester'] ?? '')); ?>
|
|
<label for="scaSemesterSelect" class="col-form-label">Semester</label>
|
|
<select id="scaSemesterSelect" name="semester" class="form-select form-select-sm" style="min-width: 140px;">
|
|
<option value="">—</option>
|
|
<option value="Fall" <?= (strcasecmp($semVal,'Fall')===0?'selected':'') ?>>Fall</option>
|
|
<option value="Spring" <?= (strcasecmp($semVal,'Spring')===0?'selected':'') ?>>Spring</option>
|
|
</select>
|
|
<button type="submit" class="btn btn-secondary btn-sm">Apply</button>
|
|
</form>
|
|
</div>
|
|
<?php if (isset($isCurrentYear) && !$isCurrentYear): ?>
|
|
<div class="col-auto"><span class="badge bg-secondary">Read-only (Past Year)</span></div>
|
|
<?php endif; ?>
|
|
</div>
|
|
|
|
<div class="table-responsive">
|
|
<!-- Auto-distribute panel -->
|
|
<div class="card mb-3">
|
|
<div class="card-body">
|
|
<form id="autoDistForm" action="<?= site_url('administrator/sections/auto-distribute') ?>" method="post" class="row g-2 align-items-end">
|
|
<?= csrf_field() ?>
|
|
<div class="col-md-4">
|
|
<label for="autoDistSection" class="form-label">Class (base section)</label>
|
|
<select id="autoDistSection" name="class_section_id" class="form-select" required>
|
|
<option value="">Select class…</option>
|
|
<?php if (!empty($classes)): ?>
|
|
<?php foreach ($classes as $c): ?>
|
|
<?php $nm = (string)($c['class_section_name'] ?? '');
|
|
$isBase = (strpos($nm, '-') === false);
|
|
if (!$isBase) continue; ?>
|
|
<option value="<?= (int)($c['class_section_id'] ?? 0) ?>">
|
|
<?= esc($nm) ?>
|
|
</option>
|
|
<?php endforeach; ?>
|
|
<?php endif; ?>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<label for="autoDistPer" class="form-label">Students per section</label>
|
|
<input type="number" min="1" id="autoDistPer" name="students_per_section" class="form-control" required />
|
|
</div>
|
|
<div class="col-md-3">
|
|
<label for="autoDistYear" class="form-label">School year</label>
|
|
<input type="text" id="autoDistYear" name="school_year" value="<?= esc($selectedYear ?? '') ?>" class="form-control" />
|
|
</div>
|
|
<div class="col-md-2 d-grid">
|
|
<button type="submit" class="btn btn-outline-primary">Auto-Distribute</button>
|
|
</div>
|
|
<div class="col-12">
|
|
<div id="autoDistMsg" class="small text-muted"></div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<table id="studentsTable" class="display table table-striped table-hover align-middle" style="width:100%">
|
|
<thead class="table-dark">
|
|
<tr>
|
|
<th>Student Name</th>
|
|
<th>Age</th>
|
|
<th>Registration Grade</th>
|
|
<th>New Student</th>
|
|
<th>Current Assigned Grade</th>
|
|
<th>Registration Date</th>
|
|
<th>School Year</th>
|
|
<th>Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php if (!empty($students)): ?>
|
|
<?php foreach ($students as $student): ?>
|
|
<tr data-student-id="<?= (int)($student['student_id'] ?? 0) ?>"
|
|
data-student-name="<?= esc($student['name'] ?? '') ?>"
|
|
data-class-ids='<?= esc(json_encode($student['class_section_ids'] ?? [])) ?>'
|
|
data-class-names='<?= esc(json_encode($student['class_section_names'] ?? [])) ?>'>
|
|
<td>
|
|
<a href="#" class="text-decoration-none" data-family-student-id="<?= (int)($student['student_id'] ?? 0) ?>">
|
|
<?= esc($student['name']) ?>
|
|
</a>
|
|
</td>
|
|
<td><?= esc($student['age']) ?></td>
|
|
<td><?= esc($student['registration_grade']) ?></td>
|
|
<td><?= esc($student['new_student']) ?></td>
|
|
|
|
<?php
|
|
$sIds = $student['class_section_ids'] ?? [];
|
|
$sNames = $student['class_section_names'] ?? [];
|
|
$sidVal = (int)($student['student_id'] ?? 0);
|
|
$badgeHtml = '';
|
|
if (!empty($sIds) && is_array($sIds)) {
|
|
foreach ($sIds as $idx => $cid) {
|
|
$nm = $sNames[$idx] ?? ('Class #'.$cid);
|
|
$badgeHtml .= '<span class="badge bg-success me-1 mb-1 d-inline-flex align-items-center gap-1">';
|
|
$badgeHtml .= '<span>'.esc($nm).'</span>';
|
|
$badgeHtml .= '<button type="button" class="btn-close btn-close-white btn-close-sm remove-class-chip" data-student-id="'.$sidVal.'" data-class-id="'.(int)$cid.'" aria-label="Remove"></button>';
|
|
$badgeHtml .= '</span>';
|
|
}
|
|
} else {
|
|
$badgeHtml = '<span class="text-muted">No class assigned</span>';
|
|
}
|
|
?>
|
|
<td class="assigned-classes" data-class-ids='<?= esc(json_encode($sIds)) ?>' data-class-names='<?= esc(json_encode($sNames)) ?>'>
|
|
<?= $badgeHtml ?>
|
|
</td>
|
|
|
|
<td data-order="<?= esc($student['registration_date']) ?>">
|
|
<?php if (!empty($student['registration_date'])): ?>
|
|
<?= local_date($student['registration_date'], 'm-d-Y') ?>
|
|
<?php else: ?>
|
|
-
|
|
<?php endif; ?>
|
|
</td>
|
|
|
|
<td><?= esc($student['school_year']) ?></td>
|
|
<td>
|
|
<button
|
|
type="button"
|
|
class="btn btn-primary btn-sm assign-student-btn" <?= (isset($isCurrentYear) && !$isCurrentYear) ? 'disabled title="Editing disabled for non-current year"' : '' ?>
|
|
data-bs-toggle="modal"
|
|
data-bs-target="#assignStudentClassModal"
|
|
data-student-id="<?= esc($student['student_id']) ?>"
|
|
data-student-name="<?= esc($student['name']) ?>"
|
|
data-current-classes="<?= esc($student['class_section_name'] ?? '') ?>"
|
|
data-class-ids="<?= esc(json_encode($student['class_section_ids'] ?? [])) ?>"
|
|
data-class-names="<?= esc(json_encode($student['class_section_names'] ?? [])) ?>">
|
|
Assign Class
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
<?php endif; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Single Modal (do NOT include another modal partial) -->
|
|
<div class="modal fade" id="assignStudentClassModal" tabindex="-1" aria-labelledby="assignStudentClassModalLabel" aria-hidden="true">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title" id="assignStudentClassModalLabel">Assign Class to Student</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
</div>
|
|
|
|
<div class="modal-body">
|
|
<div id="assignError" class="alert alert-danger d-none"></div>
|
|
<div class="mb-2 small text-muted" id="currentAssignments"></div>
|
|
|
|
<form id="assignStudentClassForm" action="<?= base_url('assign_class_student') ?>" method="post">
|
|
<?= csrf_field() ?>
|
|
<input type="hidden" name="student_id" id="studentId">
|
|
<div class="mb-3">
|
|
<label for="studentName" class="form-label">Student Name</label>
|
|
<input type="text" class="form-control" id="studentName" readonly>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label for="student_class_section_id" class="form-label">Select Class(es)</label>
|
|
<select name="class_section_id[]" id="student_class_section_id" class="form-control" multiple required size="8">
|
|
<option value="" disabled>Select one or more classes</option>
|
|
<?php if (!empty($classes) && is_array($classes)): ?>
|
|
<?php foreach ($classes as $class_): ?>
|
|
<option value="<?= htmlspecialchars($class_['class_section_id']) ?>">
|
|
<?= htmlspecialchars($class_['class_section_name']) ?>
|
|
</option>
|
|
<?php endforeach; ?>
|
|
<?php else: ?>
|
|
<option value="" disabled>No classes available</option>
|
|
<?php endif; ?>
|
|
</select>
|
|
<div class="form-text">Use Ctrl/Cmd + click to select multiple classes. Existing assignments are kept.</div>
|
|
</div>
|
|
<div class="form-check mb-2">
|
|
<input class="form-check-input" type="checkbox" value="1" id="is_event_only" name="is_event_only">
|
|
<label class="form-check-label" for="is_event_only">
|
|
Event class only (no tuition)
|
|
</label>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
|
<button type="button" class="btn btn-primary" id="assignStudentClassButton">Assign Class</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Remove Modal -->
|
|
<div class="modal fade" id="removeStudentClassModal" tabindex="-1" aria-labelledby="removeStudentClassModalLabel" aria-hidden="true">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title" id="removeStudentClassModalLabel">Remove Class from Student</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
</div>
|
|
|
|
<div class="modal-body">
|
|
<div id="removeError" class="alert alert-danger d-none"></div>
|
|
|
|
<form id="removeStudentClassForm" action="<?= site_url('remove_class_student') ?>" method="post">
|
|
<?= csrf_field() ?>
|
|
<input type="hidden" name="student_id" id="removeStudentId">
|
|
<div class="mb-3">
|
|
<label for="removeStudentName" class="form-label">Student Name</label>
|
|
<input type="text" class="form-control" id="removeStudentName" readonly>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label for="remove_class_section_id" class="form-label">Select Class to Remove</label>
|
|
<select name="class_section_id" id="remove_class_section_id" class="form-control" required>
|
|
<option value="" disabled selected>Select a class</option>
|
|
</select>
|
|
<div class="form-text">Only currently assigned classes are shown.</div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
|
<button type="button" class="btn btn-danger" id="removeStudentClassButton">Remove Class</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<?= $this->endSection() ?>
|
|
|
|
<?= $this->section('scripts') ?>
|
|
<script>
|
|
// Helpers to render assigned class chips
|
|
let CSRF_NAME = '<?= csrf_token() ?>';
|
|
let CSRF_HASH = '<?= csrf_hash() ?>';
|
|
|
|
function applyCsrfToForms(name, hash) {
|
|
if (!name || !hash) return;
|
|
$('form').each(function(){
|
|
const $f = $(this);
|
|
let $fld = $f.find('input[name="'+name+'"]');
|
|
if (!$fld.length) {
|
|
$f.find('input[type="hidden"]').filter(function(){ return this.name && this.name.startsWith('csrf_'); }).remove();
|
|
$fld = $('<input>', {type:'hidden', name});
|
|
$f.prepend($fld);
|
|
}
|
|
$fld.val(hash);
|
|
});
|
|
}
|
|
|
|
function updateCsrf($form, res){
|
|
if (!$form || !res) return;
|
|
let name = res.csrfTokenName || null;
|
|
let hash = res.csrfHash || null;
|
|
|
|
if (!hash) {
|
|
$form.find('input[type="hidden"]').each(function(){
|
|
const nm = this.name;
|
|
if (nm && res[nm]) { name = nm; hash = res[nm]; return false; }
|
|
});
|
|
}
|
|
if (name && hash) {
|
|
CSRF_NAME = name;
|
|
CSRF_HASH = hash;
|
|
applyCsrfToForms(CSRF_NAME, CSRF_HASH);
|
|
}
|
|
}
|
|
|
|
function findColIndexByHeader(text){
|
|
let idx = -1;
|
|
$('#studentsTable thead th').each(function(i){
|
|
if ($(this).text().trim() === text) idx = i;
|
|
});
|
|
return idx >= 0 ? idx : 4; // fallback
|
|
}
|
|
const CLASS_COL_INDEX = findColIndexByHeader('Current Assigned Grade');
|
|
|
|
function renderBadgesHtml(ids, names, studentId){
|
|
if (!Array.isArray(ids) || ids.length === 0) return '<span class="text-muted">No class assigned</span>';
|
|
if (!Array.isArray(names)) names = [];
|
|
return ids.map((id, idx) => {
|
|
const nm = names[idx] || ('Class #' + id);
|
|
return `<span class="badge bg-success me-1 mb-1 d-inline-flex align-items-center gap-1">
|
|
<span>${nm}</span>
|
|
<button type="button" class="btn-close btn-close-white btn-close-sm remove-class-chip" data-student-id="${studentId}" data-class-id="${id}" aria-label="Remove"></button>
|
|
</span>`;
|
|
}).join('');
|
|
}
|
|
|
|
function renderAssignedBadges() {
|
|
const decodeAttr = (raw) => {
|
|
if (!raw) return '';
|
|
return raw.replace(/"/g, '"').replace(/'/g, "'").trim();
|
|
};
|
|
$('#studentsTable tbody tr').each(function(){
|
|
const $row = $(this);
|
|
const $cell = $row.find('td').eq(CLASS_COL_INDEX);
|
|
if (!$cell.length) return;
|
|
if (!$cell.attr('data-class-ids') && $row.attr('data-class-ids')) {
|
|
$cell.attr('data-class-ids', $row.attr('data-class-ids'));
|
|
}
|
|
if (!$cell.attr('data-class-names') && $row.attr('data-class-names')) {
|
|
$cell.attr('data-class-names', $row.attr('data-class-names'));
|
|
}
|
|
const idsRaw = decodeAttr($cell.attr('data-class-ids') || $row.attr('data-class-ids') || '[]');
|
|
const namesRaw = decodeAttr($cell.attr('data-class-names') || $row.attr('data-class-names') || '[]');
|
|
let ids = [], names = [];
|
|
try { ids = JSON.parse(idsRaw) || []; } catch(_) {}
|
|
try { names = JSON.parse(namesRaw) || []; } catch(_) {}
|
|
const studentId = parseInt($row.attr('data-student-id') || '0', 10);
|
|
const badgeHtml = renderBadgesHtml(ids, names, studentId);
|
|
$cell.html(badgeHtml || $cell.html());
|
|
});
|
|
}
|
|
window.renderAssignedBadges = renderAssignedBadges;
|
|
window.renderBadgesHtml = renderBadgesHtml;
|
|
|
|
// Keep table row + button data in sync after add/remove so modals stay up-to-date without refresh.
|
|
function syncAssignButtonData($row, ids, names, displayVal){
|
|
const idsJson = JSON.stringify(ids || []);
|
|
const namesJson = JSON.stringify(names || []);
|
|
const $cell = $row.find('td').eq(CLASS_COL_INDEX);
|
|
$row.attr('data-class-ids', idsJson).attr('data-class-names', namesJson);
|
|
$cell.attr('data-class-ids', idsJson).attr('data-class-names', namesJson);
|
|
$row.find('.assign-student-btn')
|
|
.attr('data-class-ids', idsJson)
|
|
.attr('data-class-names', namesJson)
|
|
.attr('data-current-classes', displayVal || '')
|
|
.data('class-ids', ids || [])
|
|
.data('class-names', names || [])
|
|
.data('current-classes', displayVal || '');
|
|
}
|
|
window.syncAssignButtonData = syncAssignButtonData;
|
|
|
|
function getAssignmentsFromRow($row){
|
|
const res = { ids: [], names: [] };
|
|
const $cell = $row.find('td').eq(CLASS_COL_INDEX);
|
|
const $chips = $cell.find('.remove-class-chip');
|
|
if ($chips.length) {
|
|
$chips.each(function(){
|
|
const cid = parseInt($(this).data('class-id') || 0, 10);
|
|
if (cid) res.ids.push(cid);
|
|
const label = $(this).closest('.badge').clone().children('button').remove().end().text().trim();
|
|
res.names.push(label || ('Class #' + cid));
|
|
});
|
|
return res;
|
|
}
|
|
const decodeAttr = (raw) => {
|
|
if (!raw) return '';
|
|
return raw.replace(/"/g, '"').replace(/'/g, "'").trim();
|
|
};
|
|
try { res.ids = JSON.parse(decodeAttr($cell.attr('data-class-ids') || $row.attr('data-class-ids') || '[]')) || []; } catch(_) {}
|
|
try { res.names = JSON.parse(decodeAttr($cell.attr('data-class-names') || $row.attr('data-class-names') || '[]')) || []; } catch(_) {}
|
|
return res;
|
|
}
|
|
|
|
// Track the rows used by modals across handlers
|
|
let rowNode = null;
|
|
let removeRowNode = null;
|
|
|
|
(function () {
|
|
if (typeof $ === 'undefined' || !$.fn.DataTable) return;
|
|
|
|
window.studentsTable = $.fn.DataTable.isDataTable('#studentsTable')
|
|
? $('#studentsTable').DataTable()
|
|
: $('#studentsTable').DataTable({
|
|
responsive: true,
|
|
pageLength: 100,
|
|
lengthMenu: [5, 10, 25, 50, 100],
|
|
order: [[0, 'asc']],
|
|
stateSave: true,
|
|
columnDefs: [{ orderable: false, targets: [7] }],
|
|
language: { emptyTable: 'No students found.' }
|
|
});
|
|
window.studentsTable.on('draw', function(){ renderAssignedBadges(); });
|
|
window.getStudentsTable = function(){
|
|
return window.studentsTable || ($.fn.DataTable.isDataTable('#studentsTable') ? $('#studentsTable').DataTable() : null);
|
|
};
|
|
window.table = window.studentsTable; // legacy reference for existing handlers
|
|
|
|
// Open modal and set values
|
|
$(document).on('click', '.assign-student-btn', function(){
|
|
const $row = $(this).closest('tr');
|
|
rowNode = $row.get(0);
|
|
$('#studentId').val($(this).data('student-id'));
|
|
$('#studentName').val($(this).data('student-name'));
|
|
// Pre-select currently assigned classes using live badges, fallback to data attrs.
|
|
const fromRow = getAssignmentsFromRow($row);
|
|
const ids = Array.isArray(fromRow.ids) ? fromRow.ids : [];
|
|
const names = Array.isArray(fromRow.names) ? fromRow.names : [];
|
|
$('#student_class_section_id').val(ids.map(String)).trigger('change');
|
|
|
|
let current = $(this).attr('data-current-classes') || $(this).data('current-classes') || '';
|
|
if (!current && names.length) current = names.join(', ');
|
|
$('#currentAssignments').text(current ? `Currently assigned: ${current}` : 'Currently assigned: none');
|
|
$('#assignError').addClass('d-none').text('');
|
|
});
|
|
|
|
// Open remove modal and populate options
|
|
$(document).on('click', '.remove-class-btn', function(){
|
|
removeRowNode = $(this).closest('tr').get(0);
|
|
$('#removeStudentId').val($(this).data('student-id'));
|
|
$('#removeStudentName').val($(this).data('student-name'));
|
|
$('#removeError').addClass('d-none').text('');
|
|
|
|
const select = $('#remove_class_section_id');
|
|
select.empty();
|
|
select.append('<option value="" disabled selected>Select a class</option>');
|
|
try {
|
|
const ids = JSON.parse($(this).attr('data-class-ids') || '[]') || [];
|
|
const names = JSON.parse($(this).attr('data-class-names') || '[]') || [];
|
|
ids.forEach((id, idx) => {
|
|
const label = names[idx] || `Class #${id}`;
|
|
select.append(`<option value="${id}">${label}</option>`);
|
|
});
|
|
} catch (_) {
|
|
select.append('<option value="" disabled>Error loading classes</option>');
|
|
}
|
|
});
|
|
|
|
// Button click → trigger AJAX submit
|
|
$('#assignStudentClassButton').on('click', function(){
|
|
const form = document.getElementById('assignStudentClassForm');
|
|
if (form.checkValidity()) $('#assignStudentClassForm').trigger('submit');
|
|
else alert('Please select a class before submitting.');
|
|
});
|
|
|
|
// AJAX submit for assign
|
|
$(document).on('submit', '#assignStudentClassForm', function(e){
|
|
e.preventDefault();
|
|
|
|
const $form = $(this);
|
|
const url = $form.attr('action');
|
|
applyCsrfToForms(CSRF_NAME, CSRF_HASH);
|
|
|
|
const $btn = $('#assignStudentClassButton');
|
|
const orig = $btn.text();
|
|
$btn.prop('disabled', true).text('Assigning...');
|
|
|
|
const csrfField = $form.find('input[name="'+CSRF_NAME+'"]');
|
|
const csrfHeaderVal = csrfField.length ? csrfField.val() : CSRF_HASH;
|
|
|
|
$.ajax({
|
|
type: 'POST',
|
|
url: url,
|
|
data: $form.serialize(),
|
|
dataType: 'json',
|
|
headers: {
|
|
'X-Requested-With': 'XMLHttpRequest',
|
|
...(csrfHeaderVal ? {'X-CSRF-TOKEN': csrfHeaderVal} : {})
|
|
}
|
|
})
|
|
.done(function(res){
|
|
updateCsrf($form, res);
|
|
|
|
if (!res || !res.ok) {
|
|
$('#assignError').removeClass('d-none').text(res?.message || 'Unable to assign class.');
|
|
return;
|
|
}
|
|
|
|
// Update the table cell
|
|
if (rowNode) {
|
|
const tbl = window.getStudentsTable ? window.getStudentsTable() : null;
|
|
if (!tbl) return;
|
|
const displayVal = Array.isArray(res.class_section_names)
|
|
? res.class_section_names.join(', ')
|
|
: (res.class_section_name || '-');
|
|
const badgeHtml = renderBadgesHtml(res.class_section_ids || [], res.class_section_names || [], parseInt($(rowNode).attr('data-student-id') || '0', 10));
|
|
const rowIdx = tbl.row(rowNode).index();
|
|
tbl.cell(rowIdx, CLASS_COL_INDEX).data(badgeHtml);
|
|
const $row = $(tbl.row(rowIdx).node());
|
|
const $cell = $row.find('td').eq(CLASS_COL_INDEX);
|
|
$cell.html(badgeHtml);
|
|
syncAssignButtonData($row, res.class_section_ids || [], res.class_section_names || [], displayVal);
|
|
tbl.draw(false);
|
|
renderAssignedBadges();
|
|
$(rowNode).addClass('table-success');
|
|
setTimeout(()=>$(rowNode).removeClass('table-success'), 1200);
|
|
}
|
|
|
|
// Close modal
|
|
const modalEl = document.getElementById('assignStudentClassModal');
|
|
const modal = bootstrap.Modal.getInstance(modalEl);
|
|
if (modal) modal.hide();
|
|
})
|
|
.fail(function(xhr){
|
|
try {
|
|
const res = JSON.parse(xhr.responseText);
|
|
updateCsrf($form, res);
|
|
$('#assignError').removeClass('d-none').text(res?.message || 'Server error.');
|
|
} catch (_) {
|
|
$('#assignError').removeClass('d-none').text('Network or server error.');
|
|
}
|
|
})
|
|
.always(function(){
|
|
$btn.prop('disabled', false).text(orig);
|
|
});
|
|
});
|
|
})();
|
|
</script>
|
|
<script>
|
|
// Handle auto-distribute submit via AJAX
|
|
(function(){
|
|
const form = document.getElementById('autoDistForm');
|
|
if (!form) return;
|
|
form.addEventListener('submit', function(e){
|
|
e.preventDefault();
|
|
const fd = new FormData(form);
|
|
const msg = document.getElementById('autoDistMsg');
|
|
msg.textContent = 'Running distribution...';
|
|
fetch(form.action, { method: 'POST', body: fd, headers: { 'X-Requested-With':'XMLHttpRequest' } })
|
|
.then(r => r.json())
|
|
.then(res => {
|
|
msg.textContent = (res && res.message) ? res.message : 'Done.';
|
|
if (res && res.sections) {
|
|
console.log('Section summary', res.sections);
|
|
}
|
|
})
|
|
.catch(() => { msg.textContent = 'Failed to distribute.'; });
|
|
});
|
|
})();
|
|
|
|
// Handle remove class submit
|
|
(function(){
|
|
$(document).on('click', '#removeStudentClassButton', function(){
|
|
const form = document.getElementById('removeStudentClassForm');
|
|
if (form.checkValidity()) {
|
|
$('#removeStudentClassForm').trigger('submit');
|
|
} else {
|
|
alert('Please select a class to remove.');
|
|
}
|
|
});
|
|
|
|
$(document).on('submit', '#removeStudentClassForm', function(e){
|
|
e.preventDefault();
|
|
const $form = $(this);
|
|
const url = $form.attr('action');
|
|
const $btn = $('#removeStudentClassButton');
|
|
const orig = $btn.text();
|
|
$btn.prop('disabled', true).text('Removing...');
|
|
|
|
applyCsrfToForms(CSRF_NAME, CSRF_HASH);
|
|
const csrfField = $form.find('input[name="'+CSRF_NAME+'"]');
|
|
const csrfHeaderVal = csrfField.length ? csrfField.val() : CSRF_HASH;
|
|
|
|
$.ajax({
|
|
type: 'POST',
|
|
url: url,
|
|
data: $form.serialize(),
|
|
dataType: 'json',
|
|
headers: {
|
|
'X-Requested-With': 'XMLHttpRequest',
|
|
...(csrfHeaderVal ? {'X-CSRF-TOKEN': csrfHeaderVal} : {})
|
|
}
|
|
})
|
|
.done(function(res){
|
|
updateCsrf($form, res);
|
|
if (!res || !res.ok) {
|
|
$('#removeError').removeClass('d-none').text(res?.message || 'Unable to remove class.');
|
|
return;
|
|
}
|
|
|
|
// Update the table cell + data attributes
|
|
const tbl = window.getStudentsTable ? window.getStudentsTable() : null;
|
|
if (tbl) {
|
|
const targetRowNode = removeRowNode || $('#studentsTable tbody tr[data-student-id="'+(res.student_id || '')+'"]').get(0);
|
|
if (targetRowNode) {
|
|
const badgeHtml = renderBadgesHtml(res.remaining_ids || [], res.remaining_names || [], parseInt($(targetRowNode).attr('data-student-id') || '0', 10));
|
|
const rowIdx = tbl.row(targetRowNode).index();
|
|
tbl.cell(rowIdx, CLASS_COL_INDEX).data(badgeHtml);
|
|
|
|
const $row = $(tbl.row(rowIdx).node());
|
|
const $cell = $row.find('td').eq(CLASS_COL_INDEX);
|
|
$cell.html(badgeHtml); // ensure DOM shows latest without refresh
|
|
if (typeof syncAssignButtonData === 'function') {
|
|
syncAssignButtonData($row, res.remaining_ids || [], res.remaining_names || [], res.remaining_display || '');
|
|
}
|
|
tbl.draw(false); // redraw to keep ordering but stay on page
|
|
}
|
|
}
|
|
renderAssignedBadges();
|
|
const rowEl = removeRowNode || $('#studentsTable tbody tr[data-student-id="'+(res.student_id || '')+'"]').get(0);
|
|
if (rowEl) {
|
|
$(rowEl).addClass('table-warning');
|
|
setTimeout(()=>$(rowEl).removeClass('table-warning'), 1200);
|
|
}
|
|
|
|
const modalEl = document.getElementById('removeStudentClassModal');
|
|
const modal = bootstrap.Modal.getInstance(modalEl);
|
|
if (modal) modal.hide();
|
|
})
|
|
.fail(function(xhr){
|
|
try {
|
|
const res = JSON.parse(xhr.responseText);
|
|
updateCsrf($form, res);
|
|
$('#removeError').removeClass('d-none').text(res?.message || 'Server error.');
|
|
} catch (_) {
|
|
$('#removeError').removeClass('d-none').text('Network or server error.');
|
|
}
|
|
})
|
|
.always(function(){
|
|
$btn.prop('disabled', false).text(orig);
|
|
});
|
|
});
|
|
})();
|
|
|
|
// Inline chip removal (no modal)
|
|
$(document).on('click', '.remove-class-chip', function(e){
|
|
e.preventDefault();
|
|
const cid = parseInt($(this).data('class-id') || 0, 10);
|
|
const sid = parseInt($(this).data('student-id') || 0, 10);
|
|
if (!cid || !sid) return;
|
|
removeRowNode = $(this).closest('tr').get(0);
|
|
$('#removeStudentId').val(sid);
|
|
const label = $(this).closest('.badge').clone().children('button').remove().end().text().trim();
|
|
$('#remove_class_section_id').empty().append(`<option value="${cid}" selected>${label || ('Class #' + cid)}</option>`).val(cid);
|
|
$('#removeStudentClassForm').trigger('submit');
|
|
});
|
|
|
|
// Initial render of badges on page load
|
|
renderAssignedBadges();
|
|
</script>
|
|
<?= $this->endSection() ?>
|