528 lines
22 KiB
PHP
528 lines
22 KiB
PHP
<?= $this->extend('layout/management_layout') ?>
|
|
<?= $this->section('content') ?>
|
|
<div
|
|
class="container-fluid"
|
|
data-api-list="<?= esc($apiListUrl ?? '') ?>"
|
|
data-api-assign="<?= esc($apiAssignUrl ?? '') ?>"
|
|
data-api-delete="<?= esc($apiDeleteUrl ?? '') ?>"
|
|
data-csrf-name="<?= esc($csrfTokenName ?? csrf_token()) ?>"
|
|
data-csrf-value="<?= esc($csrfTokenValue ?? csrf_hash()) ?>"
|
|
>
|
|
<div class="wrapper">
|
|
<div class="content">
|
|
<h2 class="text-center mt-4 mb-2">Teacher Class Assignments</h2>
|
|
<?php if (!empty($missingYear)): ?>
|
|
<div class="alert alert-warning" role="alert">
|
|
Current school year is not configured. This page is read-only until configured in
|
|
<a href="<?= site_url('configuration/configuration_view') ?>" class="alert-link">Add/Edit Configuration</a> (key: <code>school_year</code>).
|
|
</div>
|
|
<?php endif; ?>
|
|
<div class="d-flex justify-content-center align-items-center gap-2 mb-3">
|
|
<label for="tcaYearSelect" class="col-form-label">School year</label>
|
|
<form method="get" action="<?= site_url('administrator/teacher_class_assignment') ?>" class="d-flex align-items-center gap-2">
|
|
<select id="tcaYearSelect" name="schoolYear" class="form-select form-select-sm" style="min-width: 180px;">
|
|
<?php
|
|
$years = isset($schoolYears) && is_array($schoolYears) ? $schoolYears : [];
|
|
if (empty($years) && !empty($schoolYear)) $years = [$schoolYear];
|
|
foreach ($years as $y): $val = is_array($y) && isset($y['school_year']) ? $y['school_year'] : (string)$y; ?>
|
|
<option value="<?= esc($val) ?>" <?= ((string)($schoolYear ?? '') === (string)$val) ? 'selected' : '' ?>>
|
|
<?= esc($val) ?>
|
|
</option>
|
|
<?php endforeach; ?>
|
|
</select>
|
|
<?php $semVal = (string)($semester ?? ($_GET['semester'] ?? '')); ?>
|
|
<label for="tcaSemesterSelect" class="col-form-label">Semester</label>
|
|
<select id="tcaSemesterSelect" 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>
|
|
<?php if (isset($isCurrentYear) && !$isCurrentYear): ?>
|
|
<span class="badge bg-secondary">Read-only (Past Year)</span>
|
|
<?php endif; ?>
|
|
</div>
|
|
|
|
<?= view('partials/flash_messages') ?>
|
|
<div id="assignmentMessages"></div>
|
|
|
|
<table id="teachersTable" class="display table table-striped table-hover align-middle">
|
|
<thead class="table-dark">
|
|
<tr>
|
|
<th>#</th>
|
|
<th>First Name</th>
|
|
<th>Last Name</th>
|
|
<th>Email</th>
|
|
<th>Phone Number</th>
|
|
<th>Role</th>
|
|
<th>Assigned Classes</th>
|
|
<th>School Year</th>
|
|
<th>Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr>
|
|
<td class="text-center text-muted">Loading...</td>
|
|
<td></td>
|
|
<td></td>
|
|
<td></td>
|
|
<td></td>
|
|
<td></td>
|
|
<td></td>
|
|
<td></td>
|
|
<td></td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<?= $this->include('partials/assign_class_teacher_modal') ?>
|
|
<?= $this->endSection() ?>
|
|
|
|
<?= $this->section('scripts') ?>
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', function () {
|
|
var container = document.querySelector('.container-fluid[data-api-list]');
|
|
if (!container) {
|
|
return;
|
|
}
|
|
|
|
var apiList = container.getAttribute('data-api-list') || '';
|
|
var apiAssign = container.getAttribute('data-api-assign') || '';
|
|
var apiDelete = container.getAttribute('data-api-delete') || '';
|
|
|
|
var csrfName = container.getAttribute('data-csrf-name') || '';
|
|
var csrfValue = container.getAttribute('data-csrf-value') || '';
|
|
|
|
var tableEl = document.getElementById('teachersTable');
|
|
var tbody = tableEl ? tableEl.querySelector('tbody') : null;
|
|
var messages = document.getElementById('assignmentMessages');
|
|
var selectedYear = <?= json_encode((string)($schoolYear ?? '')) ?>;
|
|
var isCurrentYear = <?= json_encode((bool)($isCurrentYear ?? true)) ?>;
|
|
var classSelect = document.getElementById('class_section_id');
|
|
var assignForm = document.getElementById('assignClassForm');
|
|
var assignButton = document.getElementById('assignClassButton');
|
|
var teacherRoleHidden = document.getElementById('teacherRoleHidden');
|
|
var teacherRoleDisplay = document.getElementById('teacherRoleDisplay');
|
|
var assignModalEl = document.getElementById('assignClassModal');
|
|
|
|
function updateCsrfFromResponse(data) {
|
|
if (!data) return;
|
|
if (data.csrf_token && data.csrf_hash) {
|
|
csrfName = data.csrf_token;
|
|
csrfValue = data.csrf_hash;
|
|
container.setAttribute('data-csrf-name', csrfName);
|
|
container.setAttribute('data-csrf-value', csrfValue);
|
|
var hiddenField = document.getElementById('assignCsrfField');
|
|
if (hiddenField) {
|
|
hiddenField.setAttribute('name', csrfName);
|
|
hiddenField.value = csrfValue;
|
|
}
|
|
}
|
|
}
|
|
|
|
function applyCsrf(payload) {
|
|
var data = Object.assign({}, payload || {});
|
|
if (csrfName) {
|
|
data[csrfName] = csrfValue;
|
|
}
|
|
return data;
|
|
}
|
|
|
|
function bootstrapAlertClass(type) {
|
|
if (type === 'success') return 'alert-success';
|
|
if (type === 'info') return 'alert-info';
|
|
if (type === 'warning') return 'alert-warning';
|
|
return 'alert-danger';
|
|
}
|
|
|
|
function showMessage(type, text) {
|
|
if (!messages || !text) return;
|
|
var alert = document.createElement('div');
|
|
alert.className = 'alert ' + bootstrapAlertClass(type) + ' alert-dismissible fade show';
|
|
alert.setAttribute('role', 'alert');
|
|
alert.innerHTML = '<span>' + text + '</span>'
|
|
+ '<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>';
|
|
messages.appendChild(alert);
|
|
setTimeout(function () {
|
|
if (alert && alert.parentNode) {
|
|
alert.classList.remove('show');
|
|
alert.classList.add('fade');
|
|
setTimeout(function () {
|
|
if (alert.parentNode) {
|
|
alert.parentNode.removeChild(alert);
|
|
}
|
|
}, 200);
|
|
}
|
|
}, 6000);
|
|
}
|
|
|
|
function renderAssignedList(assignments, label, teacher) {
|
|
var fragment = document.createDocumentFragment();
|
|
assignments.forEach(function (assignment) {
|
|
var li = document.createElement('li');
|
|
li.className = 'd-flex align-items-center justify-content-between gap-2 mb-1';
|
|
|
|
var text = document.createElement('span');
|
|
text.innerHTML = '<strong>' + (assignment.class_section_name || 'Class #' + assignment.section_id) + '</strong> (' + label + ')';
|
|
li.appendChild(text);
|
|
|
|
var btn = document.createElement('button');
|
|
btn.type = 'button';
|
|
btn.className = 'btn btn-outline-danger btn-sm remove-assignment-btn';
|
|
btn.textContent = 'Remove';
|
|
btn.setAttribute('data-teacher-id', teacher.teacher_id);
|
|
btn.setAttribute('data-teacher-name', teacher.name || '');
|
|
btn.setAttribute('data-class-section-id', assignment.section_id);
|
|
btn.setAttribute('data-class-name', assignment.class_section_name || 'Class #' + assignment.section_id);
|
|
var position = (assignment.role || '').toLowerCase();
|
|
if (position !== 'main' && position !== 'ta') {
|
|
position = label.toLowerCase() === 'ta' ? 'ta' : 'main';
|
|
}
|
|
btn.setAttribute('data-position', position);
|
|
var assignmentSemester = assignment.semester || '';
|
|
var assignmentSchoolYear = assignment.school_year || selectedYear || '';
|
|
btn.setAttribute('data-semester', assignmentSemester);
|
|
btn.setAttribute('data-school-year', assignmentSchoolYear);
|
|
li.appendChild(btn);
|
|
|
|
fragment.appendChild(li);
|
|
});
|
|
return fragment;
|
|
}
|
|
|
|
function renderTeachersTable(teachers, schoolYear) {
|
|
if (!tbody) return;
|
|
|
|
if (window.jQuery && $.fn.DataTable && $.fn.DataTable.isDataTable('#teachersTable')) {
|
|
$('#teachersTable').DataTable().destroy();
|
|
}
|
|
|
|
tbody.innerHTML = '';
|
|
|
|
if (!teachers || teachers.length === 0) {
|
|
var emptyRow = document.createElement('tr');
|
|
var emptyCell = document.createElement('td');
|
|
emptyCell.className = 'text-center text-muted';
|
|
emptyCell.textContent = 'No teachers found.';
|
|
emptyRow.appendChild(emptyCell);
|
|
for (var i = 0; i < 8; i++) {
|
|
emptyRow.appendChild(document.createElement('td'));
|
|
}
|
|
tbody.appendChild(emptyRow);
|
|
} else {
|
|
teachers.forEach(function (teacher, index) {
|
|
var row = document.createElement('tr');
|
|
|
|
var orderCell = document.createElement('td');
|
|
orderCell.className = 'text-center';
|
|
orderCell.textContent = index + 1;
|
|
row.appendChild(orderCell);
|
|
|
|
var firstCell = document.createElement('td');
|
|
firstCell.textContent = teacher.firstname || '';
|
|
row.appendChild(firstCell);
|
|
|
|
var lastCell = document.createElement('td');
|
|
lastCell.textContent = teacher.lastname || '';
|
|
row.appendChild(lastCell);
|
|
|
|
var emailCell = document.createElement('td');
|
|
emailCell.textContent = teacher.email || '';
|
|
row.appendChild(emailCell);
|
|
|
|
var phoneCell = document.createElement('td');
|
|
phoneCell.textContent = teacher.cellphone || '';
|
|
row.appendChild(phoneCell);
|
|
|
|
var roleCell = document.createElement('td');
|
|
var role = (teacher.role || '').replace(/_/g, ' ');
|
|
roleCell.textContent = role ? role.charAt(0).toUpperCase() + role.slice(1) : 'N/A';
|
|
row.appendChild(roleCell);
|
|
|
|
var assignedCell = document.createElement('td');
|
|
if ((teacher.main_assignments && teacher.main_assignments.length) ||
|
|
(teacher.ta_assignments && teacher.ta_assignments.length)) {
|
|
var list = document.createElement('ul');
|
|
list.className = 'list-unstyled mb-0';
|
|
if (teacher.main_assignments && teacher.main_assignments.length) {
|
|
list.appendChild(renderAssignedList(teacher.main_assignments, 'Main', teacher));
|
|
}
|
|
if (teacher.ta_assignments && teacher.ta_assignments.length) {
|
|
list.appendChild(renderAssignedList(teacher.ta_assignments, 'TA', teacher));
|
|
}
|
|
assignedCell.appendChild(list);
|
|
} else {
|
|
var empty = document.createElement('em');
|
|
empty.textContent = 'No classes assigned';
|
|
assignedCell.appendChild(empty);
|
|
}
|
|
row.appendChild(assignedCell);
|
|
|
|
var schoolYearCell = document.createElement('td');
|
|
schoolYearCell.textContent = teacher.school_year || schoolYear || '';
|
|
row.appendChild(schoolYearCell);
|
|
|
|
var actionsCell = document.createElement('td');
|
|
var assignBtn = document.createElement('button');
|
|
assignBtn.type = 'button';
|
|
assignBtn.className = 'btn btn-success btn-sm assign-class-btn';
|
|
if (!isCurrentYear) { assignBtn.disabled = true; assignBtn.title = 'Editing disabled for non-current year'; }
|
|
assignBtn.setAttribute('data-bs-toggle', 'modal');
|
|
assignBtn.setAttribute('data-bs-target', '#assignClassModal');
|
|
assignBtn.setAttribute('data-teacher-id', teacher.teacher_id);
|
|
assignBtn.setAttribute('data-teacher-name', teacher.name || '');
|
|
assignBtn.setAttribute('data-teacher-role', teacher.role || '');
|
|
assignBtn.textContent = 'Assign Class';
|
|
actionsCell.appendChild(assignBtn);
|
|
row.appendChild(actionsCell);
|
|
|
|
tbody.appendChild(row);
|
|
});
|
|
}
|
|
|
|
if (window.jQuery && $.fn.DataTable) {
|
|
$('#teachersTable').DataTable({
|
|
pageLength: 100,
|
|
lengthMenu: [5, 10, 25, 50, 100],
|
|
order: [[0, 'asc']],
|
|
columnDefs: [
|
|
{ orderable: false, targets: [6, 8] }
|
|
]
|
|
});
|
|
}
|
|
}
|
|
|
|
function populateClassOptions(classes) {
|
|
if (!classSelect) {
|
|
return;
|
|
}
|
|
classSelect.innerHTML = '';
|
|
|
|
var placeholder = document.createElement('option');
|
|
placeholder.value = '';
|
|
placeholder.disabled = true;
|
|
placeholder.selected = true;
|
|
placeholder.textContent = 'Select a class';
|
|
classSelect.appendChild(placeholder);
|
|
|
|
(classes || []).forEach(function (cls) {
|
|
var option = document.createElement('option');
|
|
option.value = cls.class_section_id;
|
|
option.textContent = cls.class_section_name || ('Class #' + cls.class_section_id);
|
|
classSelect.appendChild(option);
|
|
});
|
|
}
|
|
|
|
function loadAssignments() {
|
|
if (!apiList || !tbody) {
|
|
return;
|
|
}
|
|
|
|
tbody.innerHTML = '<tr>'
|
|
+ '<td class="text-center text-muted">Loading...</td>'
|
|
+ '<td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td>'
|
|
+ '</tr>';
|
|
|
|
var url = apiList + (selectedYear ? ('?schoolYear=' + encodeURIComponent(selectedYear)) : '');
|
|
fetch(url, {
|
|
headers: { 'Accept': 'application/json' },
|
|
credentials: 'same-origin'
|
|
})
|
|
.then(function (response) {
|
|
return response.json()
|
|
.catch(function () { return {}; })
|
|
.then(function (data) {
|
|
return { ok: response.ok, data: data };
|
|
});
|
|
})
|
|
.then(function (result) {
|
|
if (!result) {
|
|
throw new Error('No response received.');
|
|
}
|
|
|
|
var data = result.data || {};
|
|
updateCsrfFromResponse(data);
|
|
|
|
if (!result.ok || data.ok === false) {
|
|
showMessage('danger', data.message || 'Unable to load teacher assignments.');
|
|
return;
|
|
}
|
|
|
|
renderTeachersTable(data.teachers || [], data.school_year || '');
|
|
populateClassOptions(data.classes || []);
|
|
})
|
|
.catch(function (error) {
|
|
showMessage('danger', error.message || 'Failed to load assignments.');
|
|
});
|
|
}
|
|
|
|
function submitAssignment(payload) {
|
|
if (!apiAssign || !isCurrentYear) {
|
|
showMessage('warning', 'Editing disabled for non-current year.');
|
|
return;
|
|
}
|
|
|
|
fetch(apiAssign, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Accept': 'application/json'
|
|
},
|
|
credentials: 'same-origin',
|
|
body: JSON.stringify(applyCsrf(payload))
|
|
})
|
|
.then(function (response) {
|
|
return response.json()
|
|
.catch(function () { return {}; })
|
|
.then(function (data) {
|
|
return { ok: response.ok, data: data };
|
|
});
|
|
})
|
|
.then(function (result) {
|
|
if (!result) {
|
|
throw new Error('No response received.');
|
|
}
|
|
|
|
var data = result.data || {};
|
|
updateCsrfFromResponse(data);
|
|
|
|
if (!result.ok || data.ok === false) {
|
|
showMessage('danger', data.message || 'Unable to assign class.');
|
|
return;
|
|
}
|
|
|
|
showMessage('success', data.message || 'Class assigned successfully.');
|
|
|
|
if (assignForm) {
|
|
assignForm.reset();
|
|
}
|
|
|
|
if (assignModalEl && window.bootstrap) {
|
|
var modalInstance = bootstrap.Modal.getInstance(assignModalEl);
|
|
if (modalInstance) {
|
|
modalInstance.hide();
|
|
}
|
|
}
|
|
|
|
loadAssignments();
|
|
})
|
|
.catch(function (error) {
|
|
showMessage('danger', error.message || 'Unable to assign class.');
|
|
});
|
|
}
|
|
|
|
function removeAssignment(payload) {
|
|
if (!apiDelete || !isCurrentYear) {
|
|
showMessage('warning', 'Editing disabled for non-current year.');
|
|
return;
|
|
}
|
|
|
|
fetch(apiDelete, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Accept': 'application/json'
|
|
},
|
|
credentials: 'same-origin',
|
|
body: JSON.stringify(applyCsrf(payload))
|
|
})
|
|
.then(function (response) {
|
|
return response.json()
|
|
.catch(function () { return {}; })
|
|
.then(function (data) {
|
|
return { ok: response.ok, data: data };
|
|
});
|
|
})
|
|
.then(function (result) {
|
|
if (!result) {
|
|
throw new Error('No response received.');
|
|
}
|
|
|
|
var data = result.data || {};
|
|
updateCsrfFromResponse(data);
|
|
|
|
if (!result.ok || data.ok === false) {
|
|
showMessage('danger', data.message || 'Unable to remove assignment.');
|
|
return;
|
|
}
|
|
|
|
showMessage('success', data.message || 'Assignment removed successfully.');
|
|
loadAssignments();
|
|
})
|
|
.catch(function (error) {
|
|
showMessage('danger', error.message || 'Unable to remove assignment.');
|
|
});
|
|
}
|
|
|
|
document.addEventListener('click', function (event) {
|
|
var assignBtn = event.target.closest('.assign-class-btn');
|
|
if (assignBtn) {
|
|
var teacherId = assignBtn.getAttribute('data-teacher-id') || '';
|
|
var teacherName = assignBtn.getAttribute('data-teacher-name') || '';
|
|
var teacherRole = assignBtn.getAttribute('data-teacher-role') || '';
|
|
|
|
var nameInput = document.getElementById('teacherName');
|
|
var idInput = document.getElementById('teacherId');
|
|
|
|
if (idInput) {
|
|
idInput.value = teacherId;
|
|
}
|
|
if (nameInput) {
|
|
nameInput.value = teacherName;
|
|
}
|
|
if (teacherRoleHidden) {
|
|
teacherRoleHidden.value = teacherRole;
|
|
}
|
|
if (teacherRoleDisplay) {
|
|
teacherRoleDisplay.value = teacherRole === 'teacher_assistant' ? 'Teaching Assistant' : 'Teacher';
|
|
}
|
|
}
|
|
|
|
var removeBtn = event.target.closest('.remove-assignment-btn');
|
|
if (removeBtn) {
|
|
event.preventDefault();
|
|
var teacherId = removeBtn.getAttribute('data-teacher-id');
|
|
var classSectionId = removeBtn.getAttribute('data-class-section-id');
|
|
var position = removeBtn.getAttribute('data-position');
|
|
var semester = removeBtn.getAttribute('data-semester') || '';
|
|
var schoolYear = removeBtn.getAttribute('data-school-year') || '';
|
|
var className = removeBtn.getAttribute('data-class-name') || 'this class';
|
|
var teacherName = removeBtn.getAttribute('data-teacher-name') || 'this teacher';
|
|
|
|
if (confirm('Remove ' + className + ' (' + position.toUpperCase() + ') from ' + teacherName + '?')) {
|
|
removeAssignment({
|
|
teacher_id: teacherId,
|
|
class_section_id: classSectionId,
|
|
position: position,
|
|
semester: semester,
|
|
school_year: schoolYear
|
|
});
|
|
}
|
|
}
|
|
});
|
|
|
|
if (assignButton && assignForm) {
|
|
assignButton.addEventListener('click', function (event) {
|
|
event.preventDefault();
|
|
if (!assignForm.checkValidity()) {
|
|
assignForm.reportValidity ? assignForm.reportValidity() : alert('Please fill out all required fields.');
|
|
return;
|
|
}
|
|
|
|
submitAssignment({
|
|
teacher_id: assignForm.teacher_id.value,
|
|
teacher_role: assignForm.teacher_role ? assignForm.teacher_role.value : (teacherRoleHidden ? teacherRoleHidden.value : ''),
|
|
class_section_id: assignForm.class_section_id.value
|
|
});
|
|
});
|
|
}
|
|
|
|
loadAssignments();
|
|
});
|
|
</script>
|
|
<?= $this->endSection() ?>
|