recreate project
This commit is contained in:
@@ -0,0 +1,514 @@
|
||||
<?= $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 colspan="9" class="text-center text-muted">Loading...</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.colSpan = 9;
|
||||
emptyCell.className = 'text-center text-muted';
|
||||
emptyCell.textContent = 'No teachers found.';
|
||||
emptyRow.appendChild(emptyCell);
|
||||
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 colspan="9" class="text-center text-muted">Loading...</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() ?>
|
||||
Reference in New Issue
Block a user