This commit is contained in:
@@ -3,30 +3,44 @@
|
||||
<?= $this->section('styles') ?>
|
||||
<style>
|
||||
#classesTable {
|
||||
table-layout: fixed;
|
||||
table-layout: auto;
|
||||
}
|
||||
#classesTable th:nth-child(1),
|
||||
#classesTable td:nth-child(1) {
|
||||
width: 10rem;
|
||||
width: 1%;
|
||||
white-space: nowrap;
|
||||
}
|
||||
#classesTable th:nth-child(2),
|
||||
#classesTable td:nth-child(2),
|
||||
#classesTable td:nth-child(2) {
|
||||
width: 1%;
|
||||
white-space: nowrap;
|
||||
}
|
||||
#classesTable th:nth-child(3),
|
||||
#classesTable td:nth-child(3) {
|
||||
width: 6rem;
|
||||
}
|
||||
#classesTable th:nth-child(4),
|
||||
#classesTable td:nth-child(4) {
|
||||
width: 10rem;
|
||||
min-width: 34rem;
|
||||
}
|
||||
.distribution-cell {
|
||||
min-width: 28rem;
|
||||
min-width: 34rem;
|
||||
}
|
||||
.distribution-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr));
|
||||
gap: .5rem;
|
||||
}
|
||||
.distribution-class-roster {
|
||||
border: 1px solid #ced4da;
|
||||
border-radius: .375rem;
|
||||
background: #f8f9fa;
|
||||
padding: .5rem;
|
||||
margin-bottom: .5rem;
|
||||
}
|
||||
.distribution-class-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: .5rem;
|
||||
margin-bottom: .35rem;
|
||||
}
|
||||
.distribution-section {
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: .375rem;
|
||||
@@ -53,14 +67,78 @@
|
||||
max-height: 8.5rem;
|
||||
overflow: auto;
|
||||
margin: 0;
|
||||
padding-left: 1.1rem;
|
||||
padding-left: 0;
|
||||
list-style: none;
|
||||
counter-reset: distribution-student;
|
||||
font-size: .8125rem;
|
||||
line-height: 1.35;
|
||||
}
|
||||
.distribution-students li {
|
||||
counter-increment: distribution-student;
|
||||
}
|
||||
.distribution-class-roster .distribution-students {
|
||||
max-height: 14rem;
|
||||
}
|
||||
.distribution-students-wrap {
|
||||
min-width: 0;
|
||||
}
|
||||
.distribution-students-header {
|
||||
display: grid;
|
||||
grid-template-columns: 2.25rem minmax(12rem, 1fr) 3.5rem 5rem minmax(8rem, 10rem) minmax(12rem, 16rem);
|
||||
gap: .5rem;
|
||||
padding: 0 .25rem .2rem 0;
|
||||
font-size: .72rem;
|
||||
font-weight: 700;
|
||||
color: #6c757d;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.distribution-students-header span:nth-child(3) {
|
||||
text-align: end;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.distribution-student-row {
|
||||
display: grid;
|
||||
grid-template-columns: 2.25rem minmax(12rem, 1fr) 3.5rem 5rem minmax(8rem, 10rem) minmax(12rem, 16rem);
|
||||
gap: .5rem;
|
||||
align-items: center;
|
||||
margin-bottom: .25rem;
|
||||
}
|
||||
.student-row-number {
|
||||
color: #6c757d;
|
||||
text-align: end;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.student-row-number::before {
|
||||
content: counter(distribution-student) ". ";
|
||||
}
|
||||
.student-age-cell {
|
||||
text-align: end;
|
||||
color: #495057;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.student-gender-cell {
|
||||
color: #495057;
|
||||
}
|
||||
.student-last-year-cell {
|
||||
color: #495057;
|
||||
min-width: 0;
|
||||
}
|
||||
.student-assignment-cell {
|
||||
min-width: 0;
|
||||
}
|
||||
.student-assignment-cell .form-select {
|
||||
display: inline-block;
|
||||
width: fit-content;
|
||||
min-width: 5rem;
|
||||
max-width: 100%;
|
||||
}
|
||||
.distribution-empty {
|
||||
color: #6c757d;
|
||||
font-size: .875rem;
|
||||
}
|
||||
.distribution-search-summary {
|
||||
min-height: 1.25rem;
|
||||
}
|
||||
</style>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
@@ -104,7 +182,7 @@
|
||||
$name = trim((string)($c['class_section_name'] ?? ''));
|
||||
$isBase = ($name !== '' && strpos($name, '-') === false);
|
||||
$lowerName = strtolower($name);
|
||||
$isStandard = ($lowerName === 'kg' || $lowerName === 'youth' || (ctype_digit($lowerName) && (int)$lowerName >= 1 && (int)$lowerName <= 9));
|
||||
$isStandard = ($lowerName === 'kg' || $lowerName === 'youth' || (ctype_digit($lowerName) && (int)$lowerName >= 1 && (int)$lowerName <= 10));
|
||||
if (!$isBase || $isStandard) continue;
|
||||
?>
|
||||
<option value="<?= (int)($c['class_id'] ?? 0) ?>"><?= esc($name) ?></option>
|
||||
@@ -119,6 +197,15 @@
|
||||
<div id="includedClasses" class="d-flex flex-wrap gap-2"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row g-3 align-items-end mb-3">
|
||||
<div class="col-md-4">
|
||||
<label for="studentSearch" class="form-label">Search Students</label>
|
||||
<input type="search" id="studentSearch" class="form-control" placeholder="Search by student name..." autocomplete="off" />
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<div id="studentSearchSummary" class="small text-muted distribution-search-summary"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm table-striped align-middle no-mgmt-sticky" id="classesTable">
|
||||
@@ -126,8 +213,6 @@
|
||||
<tr id="tblHeader">
|
||||
<th>Class</th>
|
||||
<th>Total</th>
|
||||
<th>Sections</th>
|
||||
<th>Actions</th>
|
||||
<th>Distribution</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -151,6 +236,15 @@
|
||||
const selectedYear = '<?= esc($selectedYear ?? '') ?>';
|
||||
const totalsBaseUrl = '<?= site_url('administrator/sections/promotion-totals') ?>';
|
||||
const distUrl = '<?= site_url('administrator/sections/auto-distribute') ?>';
|
||||
const updateDraftUrl = '<?= site_url('administrator/sections/distribution-draft/update') ?>';
|
||||
const updateCandidateUrl = '<?= site_url('administrator/sections/distribution-candidate/update') ?>';
|
||||
const classSections = <?= json_encode(array_values(array_map(static function ($c) {
|
||||
return [
|
||||
'class_id' => (int)($c['class_id'] ?? 0),
|
||||
'class_section_id' => (int)($c['class_section_id'] ?? 0),
|
||||
'class_section_name' => (string)($c['class_section_name'] ?? ''),
|
||||
];
|
||||
}, $classes ?? [])), JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT) ?>;
|
||||
|
||||
const tblBody = document.getElementById('tblBody');
|
||||
const sectionCountInput = document.getElementById('sectionCount');
|
||||
@@ -161,9 +255,13 @@
|
||||
const additionalClassSelect = document.getElementById('additionalClassSelect');
|
||||
const addClassBtn = document.getElementById('addClassBtn');
|
||||
const includedClassesEl = document.getElementById('includedClasses');
|
||||
const studentSearchInput = document.getElementById('studentSearch');
|
||||
const studentSearchSummary = document.getElementById('studentSearchSummary');
|
||||
|
||||
let rowIndexByClassId = {}; // mapping to locate rows
|
||||
let includedClassIds = [];
|
||||
const baseClasses = buildBaseClassOptions();
|
||||
const sectionsByClassId = buildSectionsByClassId();
|
||||
|
||||
function totalsUrl() {
|
||||
const params = new URLSearchParams();
|
||||
@@ -172,9 +270,111 @@
|
||||
return totalsBaseUrl + '?' + params.toString();
|
||||
}
|
||||
|
||||
function selectedSectionCount() {
|
||||
const count = parseInt(sectionCountInput.value || '0', 10);
|
||||
return count > 0 ? count : '';
|
||||
function buildBaseClassOptions() {
|
||||
const seen = {};
|
||||
return classSections
|
||||
.filter(row => row.class_id > 0 && row.class_section_name && row.class_section_name.indexOf('-') < 0)
|
||||
.filter(function(row){
|
||||
if (seen[row.class_id]) return false;
|
||||
seen[row.class_id] = true;
|
||||
return true;
|
||||
})
|
||||
.sort((a, b) => String(a.class_section_name).localeCompare(String(b.class_section_name), undefined, { numeric: true }));
|
||||
}
|
||||
|
||||
function buildSectionsByClassId() {
|
||||
const out = {};
|
||||
classSections.forEach(function(row){
|
||||
if (!row.class_id || !row.class_section_id || !row.class_section_name || row.class_section_name.indexOf('-') < 0) return;
|
||||
if (!out[row.class_id]) out[row.class_id] = [];
|
||||
out[row.class_id].push({
|
||||
id: row.class_section_id,
|
||||
name: row.class_section_name
|
||||
});
|
||||
});
|
||||
Object.keys(out).forEach(function(classId){
|
||||
out[classId].sort((a, b) => String(a.name).localeCompare(String(b.name), undefined, { numeric: true }));
|
||||
});
|
||||
return out;
|
||||
}
|
||||
|
||||
function classNameById(classId) {
|
||||
const row = baseClasses.find(c => c.class_id === parseInt(classId || '0', 10));
|
||||
return row ? row.class_section_name : ('Class #' + classId);
|
||||
}
|
||||
|
||||
function isStandardBaseClassName(name) {
|
||||
const normalized = String(name || '').trim().toLowerCase();
|
||||
if (normalized === 'kg' || normalized === 'youth') return true;
|
||||
if (!/^\d+$/.test(normalized)) return false;
|
||||
const n = parseInt(normalized, 10);
|
||||
return n >= 1 && n <= 10;
|
||||
}
|
||||
|
||||
function ensureIncludedClassVisible(classId) {
|
||||
classId = parseInt(classId || '0', 10);
|
||||
if (!classId) return;
|
||||
const className = classNameById(classId);
|
||||
if (isStandardBaseClassName(className)) return;
|
||||
if (includedClassIds.indexOf(classId) < 0) {
|
||||
includedClassIds.push(classId);
|
||||
renderIncludedClasses();
|
||||
}
|
||||
}
|
||||
|
||||
function populateDestinationSelect(select, selectedSectionId, selectedClassId) {
|
||||
select.innerHTML = '';
|
||||
selectedClassId = parseInt(selectedClassId || '0', 10);
|
||||
let resolvedClassId = 0;
|
||||
|
||||
baseClasses.forEach(function(baseClass){
|
||||
const classOption = document.createElement('option');
|
||||
classOption.value = String(baseClass.class_section_id);
|
||||
classOption.dataset.classId = String(baseClass.class_id);
|
||||
classOption.textContent = baseClass.class_section_name;
|
||||
if (baseClass.class_section_id === parseInt(selectedSectionId || '0', 10) || (!parseInt(selectedSectionId || '0', 10) && selectedClassId === baseClass.class_id)) {
|
||||
classOption.selected = true;
|
||||
resolvedClassId = baseClass.class_id;
|
||||
}
|
||||
select.appendChild(classOption);
|
||||
|
||||
const sections = sectionsByClassId[String(baseClass.class_id)] || [];
|
||||
sections.forEach(function(section){
|
||||
const opt = document.createElement('option');
|
||||
opt.value = String(section.id);
|
||||
opt.dataset.classId = String(baseClass.class_id);
|
||||
opt.textContent = section.name;
|
||||
if (section.id === parseInt(selectedSectionId || '0', 10)) {
|
||||
opt.selected = true;
|
||||
resolvedClassId = baseClass.class_id;
|
||||
}
|
||||
select.appendChild(opt);
|
||||
});
|
||||
});
|
||||
|
||||
const selectedOption = select.options[select.selectedIndex] || null;
|
||||
if (!resolvedClassId && selectedOption) {
|
||||
resolvedClassId = parseInt(selectedOption.dataset.classId || '0', 10);
|
||||
}
|
||||
|
||||
return {
|
||||
classId: resolvedClassId,
|
||||
sectionId: selectedOption ? parseInt(selectedOption.value || '0', 10) : 0
|
||||
};
|
||||
}
|
||||
|
||||
function selectedDestination(select) {
|
||||
const selectedOption = select && select.options ? select.options[select.selectedIndex] : null;
|
||||
return {
|
||||
classId: selectedOption ? parseInt(selectedOption.dataset.classId || '0', 10) : 0,
|
||||
sectionId: selectedOption ? parseInt(selectedOption.value || '0', 10) : 0
|
||||
};
|
||||
}
|
||||
|
||||
function assignmentLabel(assignment) {
|
||||
if (assignment && assignment.class_section_name) return assignment.class_section_name;
|
||||
const classId = parseInt((assignment && assignment.class_id) || '0', 10);
|
||||
return classId > 0 ? classNameById(classId) : '-';
|
||||
}
|
||||
|
||||
function buildInitialTable(rows) {
|
||||
@@ -196,25 +396,16 @@
|
||||
tdTotal.textContent = r.total;
|
||||
tr.appendChild(tdTotal);
|
||||
|
||||
const tdNeed = document.createElement('td');
|
||||
tdNeed.className = 'text-end need-cell';
|
||||
tdNeed.textContent = selectedSectionCount();
|
||||
tr.appendChild(tdNeed);
|
||||
|
||||
const tdAct = document.createElement('td');
|
||||
const btn = document.createElement('button');
|
||||
btn.className = 'btn btn-sm btn-primary';
|
||||
btn.textContent = 'Generate Sections';
|
||||
btn.addEventListener('click', function(){ runDistribution(r.class_section_id, r.class_section_name); });
|
||||
tdAct.appendChild(btn);
|
||||
tr.appendChild(tdAct);
|
||||
|
||||
const tdResults = document.createElement('td');
|
||||
tdResults.className = 'distribution-cell';
|
||||
const empty = document.createElement('span');
|
||||
empty.className = 'distribution-empty';
|
||||
empty.textContent = 'Not generated';
|
||||
tdResults.appendChild(empty);
|
||||
if (Array.isArray(r.students) && r.students.length) {
|
||||
tdResults.appendChild(renderClassRoster(tr, r.students));
|
||||
} else {
|
||||
const empty = document.createElement('span');
|
||||
empty.className = 'distribution-empty';
|
||||
empty.textContent = 'No students';
|
||||
tdResults.appendChild(empty);
|
||||
}
|
||||
tr.appendChild(tdResults);
|
||||
|
||||
tblBody.appendChild(tr);
|
||||
@@ -223,16 +414,10 @@
|
||||
|
||||
rows.forEach(function(r){
|
||||
if (Array.isArray(r.sections) && r.sections.length) {
|
||||
renderSectionsForRow(r.class_section_id, r.sections);
|
||||
renderSectionsForRow(r.class_section_id, r.sections, r.students || []);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function updateNeeds() {
|
||||
document.querySelectorAll('#tblBody tr').forEach(function(tr){
|
||||
const needCell = tr.querySelector('.need-cell');
|
||||
if (needCell) needCell.textContent = selectedSectionCount();
|
||||
});
|
||||
applyStudentSearch();
|
||||
}
|
||||
|
||||
function runDistribution(baseSectionId, baseName) {
|
||||
@@ -273,12 +458,12 @@
|
||||
|
||||
msgEl.textContent = res && res.message ? res.message : 'Completed.';
|
||||
|
||||
renderSectionsForRow(baseSectionId, res.sections);
|
||||
renderSectionsForRow(baseSectionId, res.sections, []);
|
||||
})
|
||||
.catch(() => { msgEl.textContent = 'Failed to distribute. Please try again.'; });
|
||||
}
|
||||
|
||||
function renderSectionsForRow(baseSectionId, sections) {
|
||||
function renderSectionsForRow(baseSectionId, sections, unassignedAssignments) {
|
||||
const tr = Array.from(document.querySelectorAll('#tblBody tr')).find(function(_tr){
|
||||
return String(_tr.dataset.classSectionId || '') === String(baseSectionId || '');
|
||||
});
|
||||
@@ -288,11 +473,54 @@
|
||||
if (!td) return;
|
||||
td.innerHTML = '';
|
||||
|
||||
const unassigned = Array.isArray(unassignedAssignments) ? unassignedAssignments : (tr._unassignedAssignments || []);
|
||||
tr._unassignedAssignments = unassigned;
|
||||
const allSections = Array.isArray(sections) ? sections.slice() : [];
|
||||
const baseSections = allSections.filter(function(section){
|
||||
const name = String(section.class_section_name || '');
|
||||
return name === String(tr.dataset.className || '') || name.indexOf('-') < 0;
|
||||
});
|
||||
const visibleSections = allSections.filter(function(section){
|
||||
return baseSections.indexOf(section) < 0;
|
||||
});
|
||||
const baseAssignments = assignmentsFromSections(baseSections);
|
||||
const rosterAssignments = unassigned.concat(baseAssignments);
|
||||
if (rosterAssignments.length) {
|
||||
td.appendChild(renderClassRoster(tr, rosterAssignments));
|
||||
}
|
||||
|
||||
const grid = document.createElement('div');
|
||||
grid.className = 'distribution-grid';
|
||||
const allAssignments = assignmentsFromSections(visibleSections);
|
||||
|
||||
sections.forEach(function(s){
|
||||
const studentNames = Array.isArray(s.student_names) ? s.student_names : [];
|
||||
if (!visibleSections.length && !rosterAssignments.length) {
|
||||
td.appendChild(renderClassRoster(tr, allAssignments));
|
||||
}
|
||||
|
||||
visibleSections.forEach(function(s){
|
||||
const studentAssignments = Array.isArray(s.student_assignments) && s.student_assignments.length
|
||||
? s.student_assignments.map(function(assignment){
|
||||
return {
|
||||
...assignment,
|
||||
class_id: assignment.class_id || s.class_id,
|
||||
class_section_id: assignment.class_section_id || s.class_section_id,
|
||||
class_section_name: assignment.class_section_name || s.class_section_name || ''
|
||||
};
|
||||
})
|
||||
: (Array.isArray(s.student_names) ? s.student_names : []).map(function(studentName){
|
||||
return {
|
||||
draft_id: 0,
|
||||
student_id: 0,
|
||||
student_name: studentName,
|
||||
age_at_reference: null,
|
||||
gender: '',
|
||||
last_year_class_section: '',
|
||||
class_id: s.class_id,
|
||||
class_section_id: s.class_section_id,
|
||||
class_section_name: s.class_section_name || ''
|
||||
};
|
||||
});
|
||||
const studentNames = studentAssignments.map(a => a.student_name).filter(Boolean);
|
||||
const block = document.createElement('div');
|
||||
block.className = 'distribution-section';
|
||||
|
||||
@@ -327,14 +555,7 @@
|
||||
if (meta.children.length) block.appendChild(meta);
|
||||
|
||||
if (studentNames.length) {
|
||||
const list = document.createElement('ol');
|
||||
list.className = 'distribution-students';
|
||||
studentNames.forEach(function(studentName){
|
||||
const item = document.createElement('li');
|
||||
item.textContent = studentName;
|
||||
list.appendChild(item);
|
||||
});
|
||||
block.appendChild(list);
|
||||
block.appendChild(renderAssignmentList(studentAssignments, true));
|
||||
} else {
|
||||
const empty = document.createElement('div');
|
||||
empty.className = 'distribution-empty';
|
||||
@@ -346,6 +567,344 @@
|
||||
});
|
||||
|
||||
td.appendChild(grid);
|
||||
renderAddSectionButton(td, tr, baseSectionId, visibleSections);
|
||||
applyStudentSearch();
|
||||
}
|
||||
|
||||
function assignmentsFromSections(sections) {
|
||||
const out = [];
|
||||
const seenDrafts = {};
|
||||
const seenFallback = {};
|
||||
|
||||
sections.forEach(function(section){
|
||||
const assignments = Array.isArray(section.student_assignments) && section.student_assignments.length
|
||||
? section.student_assignments
|
||||
: (Array.isArray(section.student_names) ? section.student_names : []).map(function(studentName, idx){
|
||||
return {
|
||||
draft_id: 0,
|
||||
student_id: 0,
|
||||
student_name: studentName,
|
||||
age_at_reference: null,
|
||||
gender: '',
|
||||
last_year_class_section: '',
|
||||
class_id: section.class_id,
|
||||
class_section_id: section.class_section_id,
|
||||
class_section_name: section.class_section_name,
|
||||
fallback_key: String(section.class_section_id || '') + ':' + idx + ':' + String(studentName || '')
|
||||
};
|
||||
});
|
||||
|
||||
assignments.forEach(function(assignment){
|
||||
const draftId = parseInt(assignment.draft_id || '0', 10);
|
||||
if (draftId > 0) {
|
||||
if (seenDrafts[draftId]) return;
|
||||
seenDrafts[draftId] = true;
|
||||
} else {
|
||||
const key = assignment.fallback_key || String(assignment.class_section_id || section.class_section_id || '') + ':' + String(assignment.student_name || '');
|
||||
if (seenFallback[key]) return;
|
||||
seenFallback[key] = true;
|
||||
}
|
||||
|
||||
out.push({
|
||||
draft_id: draftId,
|
||||
student_id: parseInt(assignment.student_id || '0', 10),
|
||||
student_name: assignment.student_name || 'Student',
|
||||
age_at_reference: assignment.age_at_reference ?? null,
|
||||
gender: assignment.gender || '',
|
||||
last_year_class_section: assignment.last_year_class_section || '',
|
||||
class_id: parseInt(assignment.class_id || section.class_id || '0', 10),
|
||||
class_section_id: parseInt(assignment.class_section_id || section.class_section_id || '0', 10),
|
||||
class_section_name: assignment.class_section_name || section.class_section_name || ''
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
out.sort((a, b) => String(a.student_name).localeCompare(String(b.student_name), undefined, { numeric: true }));
|
||||
return out;
|
||||
}
|
||||
|
||||
function renderClassRoster(tr, assignments) {
|
||||
const wrap = document.createElement('div');
|
||||
wrap.className = 'distribution-class-roster';
|
||||
|
||||
const title = document.createElement('div');
|
||||
title.className = 'distribution-class-title';
|
||||
|
||||
const name = document.createElement('div');
|
||||
name.className = 'fw-semibold';
|
||||
name.textContent = 'Students in ' + (tr.dataset.className || 'class');
|
||||
title.appendChild(name);
|
||||
|
||||
const count = document.createElement('div');
|
||||
count.className = 'badge bg-secondary';
|
||||
count.textContent = assignments.length + ' students';
|
||||
title.appendChild(count);
|
||||
wrap.appendChild(title);
|
||||
|
||||
if (!assignments.length) {
|
||||
const empty = document.createElement('div');
|
||||
empty.className = 'distribution-empty';
|
||||
empty.textContent = 'No students assigned';
|
||||
wrap.appendChild(empty);
|
||||
return wrap;
|
||||
}
|
||||
|
||||
wrap.appendChild(renderAssignmentList(assignments, true));
|
||||
return wrap;
|
||||
}
|
||||
|
||||
function renderAssignmentList(assignments, editable) {
|
||||
const wrap = document.createElement('div');
|
||||
wrap.className = 'distribution-students-wrap';
|
||||
|
||||
const header = document.createElement('div');
|
||||
header.className = 'distribution-students-header';
|
||||
['', 'Student', 'Age', 'Gender', 'Last Year', 'Assignment'].forEach(function(label){
|
||||
const cell = document.createElement('span');
|
||||
cell.textContent = label;
|
||||
header.appendChild(cell);
|
||||
});
|
||||
wrap.appendChild(header);
|
||||
|
||||
const list = document.createElement('ol');
|
||||
list.className = 'distribution-students';
|
||||
|
||||
const sortedAssignments = Array.isArray(assignments)
|
||||
? assignments.slice().sort(function(a, b){
|
||||
return String(a.student_name || '').localeCompare(String(b.student_name || ''), undefined, { numeric: true });
|
||||
})
|
||||
: [];
|
||||
|
||||
sortedAssignments.forEach(function(assignment){
|
||||
const item = document.createElement('li');
|
||||
item.dataset.searchText = [
|
||||
assignment.student_name || '',
|
||||
assignment.age_at_reference ?? '',
|
||||
assignment.gender || '',
|
||||
assignment.last_year_class_section || '',
|
||||
assignmentLabel(assignment)
|
||||
].join(' ').toLowerCase();
|
||||
const row = document.createElement('div');
|
||||
row.className = 'distribution-student-row';
|
||||
|
||||
const rowNumber = document.createElement('span');
|
||||
rowNumber.className = 'student-row-number';
|
||||
row.appendChild(rowNumber);
|
||||
|
||||
const studentLabel = document.createElement('span');
|
||||
studentLabel.textContent = assignment.student_name || 'Student';
|
||||
row.appendChild(studentLabel);
|
||||
|
||||
const ageCell = document.createElement('span');
|
||||
ageCell.className = 'student-age-cell';
|
||||
ageCell.textContent = assignment.age_at_reference === null || assignment.age_at_reference === undefined || assignment.age_at_reference === ''
|
||||
? '-'
|
||||
: assignment.age_at_reference;
|
||||
row.appendChild(ageCell);
|
||||
|
||||
const genderCell = document.createElement('span');
|
||||
genderCell.className = 'student-gender-cell';
|
||||
genderCell.textContent = assignment.gender || '-';
|
||||
row.appendChild(genderCell);
|
||||
|
||||
const lastYearCell = document.createElement('span');
|
||||
lastYearCell.className = 'student-last-year-cell';
|
||||
lastYearCell.textContent = assignment.last_year_class_section || '-';
|
||||
row.appendChild(lastYearCell);
|
||||
|
||||
const assignmentCell = document.createElement('span');
|
||||
assignmentCell.className = 'student-assignment-cell';
|
||||
|
||||
const draftId = parseInt(assignment.draft_id || '0', 10);
|
||||
const studentId = parseInt(assignment.student_id || '0', 10);
|
||||
if (editable && (draftId > 0 || studentId > 0) && baseClasses.length) {
|
||||
const destinationSelect = document.createElement('select');
|
||||
destinationSelect.className = 'form-select form-select-sm';
|
||||
destinationSelect.setAttribute('aria-label', 'Change class and section for ' + (assignment.student_name || 'student'));
|
||||
populateDestinationSelect(
|
||||
destinationSelect,
|
||||
parseInt(assignment.class_section_id || '0', 10),
|
||||
parseInt(assignment.class_id || '0', 10)
|
||||
);
|
||||
destinationSelect.dataset.previousValue = destinationSelect.value;
|
||||
|
||||
destinationSelect.addEventListener('change', function(){
|
||||
const target = selectedDestination(destinationSelect);
|
||||
if (draftId > 0) {
|
||||
updateDraftAssignment(draftId, target.classId, target.sectionId, destinationSelect);
|
||||
} else {
|
||||
updateCandidateAssignment(studentId, target.classId, target.sectionId, destinationSelect);
|
||||
}
|
||||
});
|
||||
|
||||
assignmentCell.appendChild(destinationSelect);
|
||||
} else {
|
||||
assignmentCell.textContent = assignmentLabel(assignment);
|
||||
}
|
||||
row.appendChild(assignmentCell);
|
||||
|
||||
item.appendChild(row);
|
||||
list.appendChild(item);
|
||||
});
|
||||
|
||||
wrap.appendChild(list);
|
||||
return wrap;
|
||||
}
|
||||
|
||||
function applyStudentSearch() {
|
||||
const query = String((studentSearchInput && studentSearchInput.value) || '').trim().toLowerCase();
|
||||
let totalStudents = 0;
|
||||
let visibleStudents = 0;
|
||||
|
||||
document.querySelectorAll('.distribution-students li').forEach(function(item){
|
||||
totalStudents++;
|
||||
const matches = !query || String(item.dataset.searchText || '').indexOf(query) >= 0;
|
||||
item.hidden = !matches;
|
||||
if (matches) visibleStudents++;
|
||||
});
|
||||
|
||||
document.querySelectorAll('.distribution-section, .distribution-class-roster').forEach(function(block){
|
||||
const items = Array.from(block.querySelectorAll('.distribution-students li'));
|
||||
block.hidden = !!query && items.length > 0 && !items.some(item => !item.hidden);
|
||||
});
|
||||
|
||||
document.querySelectorAll('#tblBody tr').forEach(function(tr){
|
||||
const items = Array.from(tr.querySelectorAll('.distribution-students li'));
|
||||
tr.hidden = !!query && items.length > 0 && !items.some(item => !item.hidden);
|
||||
});
|
||||
|
||||
if (studentSearchSummary) {
|
||||
studentSearchSummary.textContent = query
|
||||
? visibleStudents + ' of ' + totalStudents + ' students shown'
|
||||
: '';
|
||||
}
|
||||
}
|
||||
|
||||
function renderAddSectionButton(container, tr, baseSectionId, visibleSections) {
|
||||
const baseClassId = parseInt(tr.dataset.classId || '0', 10);
|
||||
const allSections = sectionsByClassId[String(baseClassId)] || [];
|
||||
const visibleSectionIds = visibleSections.map(section => parseInt(section.class_section_id || '0', 10));
|
||||
const nextSection = allSections.find(section => visibleSectionIds.indexOf(section.id) < 0);
|
||||
if (!nextSection) return;
|
||||
|
||||
const addWrap = document.createElement('div');
|
||||
addWrap.className = 'mt-2';
|
||||
|
||||
const addBtn = document.createElement('button');
|
||||
addBtn.type = 'button';
|
||||
addBtn.className = 'btn btn-sm btn-outline-secondary';
|
||||
addBtn.textContent = 'Add Section';
|
||||
addBtn.addEventListener('click', function(){
|
||||
const nextVisibleSections = visibleSections.concat([{
|
||||
class_id: baseClassId,
|
||||
class_section_id: nextSection.id,
|
||||
class_section_name: nextSection.name,
|
||||
total: 0,
|
||||
student_names: [],
|
||||
student_assignments: []
|
||||
}]);
|
||||
renderSectionsForRow(baseSectionId, nextVisibleSections, tr._unassignedAssignments || []);
|
||||
});
|
||||
|
||||
addWrap.appendChild(addBtn);
|
||||
container.appendChild(addWrap);
|
||||
}
|
||||
|
||||
function updateDraftAssignment(draftId, classId, classSectionId, selectEl) {
|
||||
if (!draftId || !classSectionId) {
|
||||
msgEl.textContent = 'Select a valid target section.';
|
||||
return;
|
||||
}
|
||||
|
||||
const previousValue = selectEl ? selectEl.dataset.previousValue || selectEl.defaultValue || '' : '';
|
||||
if (selectEl) selectEl.disabled = true;
|
||||
|
||||
const fd = new FormData();
|
||||
fd.append('draft_id', String(draftId));
|
||||
fd.append('class_section_id', String(classSectionId));
|
||||
|
||||
const csrfNameEl = document.getElementById('csrfName');
|
||||
const csrfValueEl= document.getElementById('csrfValue');
|
||||
if (csrfNameEl && csrfValueEl) {
|
||||
fd.append(csrfNameEl.value, csrfValueEl.value);
|
||||
}
|
||||
|
||||
msgEl.textContent = 'Updating draft assignment...';
|
||||
fetch(updateDraftUrl, { method: 'POST', headers: { 'X-Requested-With': 'XMLHttpRequest' }, body: fd })
|
||||
.then(r => r.json())
|
||||
.then(res => {
|
||||
if (res && res.csrfTokenName && res.csrfHash) {
|
||||
if (csrfNameEl) csrfNameEl.value = res.csrfTokenName;
|
||||
if (csrfValueEl) csrfValueEl.value = res.csrfHash;
|
||||
}
|
||||
|
||||
if (!res || !res.ok) {
|
||||
if (selectEl && previousValue) selectEl.value = previousValue;
|
||||
msgEl.textContent = res && res.message ? res.message : 'Draft assignment could not be updated.';
|
||||
return;
|
||||
}
|
||||
|
||||
msgEl.textContent = res.message || 'Draft assignment updated.';
|
||||
if (selectEl) selectEl.dataset.previousValue = String(classSectionId);
|
||||
ensureIncludedClassVisible(classId);
|
||||
loadTotals();
|
||||
})
|
||||
.catch(() => {
|
||||
if (selectEl && previousValue) selectEl.value = previousValue;
|
||||
msgEl.textContent = 'Draft assignment could not be updated.';
|
||||
})
|
||||
.finally(() => {
|
||||
if (selectEl) selectEl.disabled = false;
|
||||
});
|
||||
}
|
||||
|
||||
function updateCandidateAssignment(studentId, classId, classSectionId, selectEl) {
|
||||
if (!studentId || !classSectionId) {
|
||||
msgEl.textContent = 'Select a valid student and target assignment.';
|
||||
return;
|
||||
}
|
||||
|
||||
const previousValue = selectEl ? selectEl.dataset.previousValue || selectEl.defaultValue || '' : '';
|
||||
if (selectEl) selectEl.disabled = true;
|
||||
|
||||
const fd = new FormData();
|
||||
fd.append('student_id', String(studentId));
|
||||
fd.append('class_section_id', String(classSectionId));
|
||||
fd.append('school_year', selectedYear);
|
||||
|
||||
const csrfNameEl = document.getElementById('csrfName');
|
||||
const csrfValueEl= document.getElementById('csrfValue');
|
||||
if (csrfNameEl && csrfValueEl) {
|
||||
fd.append(csrfNameEl.value, csrfValueEl.value);
|
||||
}
|
||||
|
||||
msgEl.textContent = 'Saving candidate assignment...';
|
||||
fetch(updateCandidateUrl, { method: 'POST', headers: { 'X-Requested-With': 'XMLHttpRequest' }, body: fd })
|
||||
.then(r => r.json())
|
||||
.then(res => {
|
||||
if (res && res.csrfTokenName && res.csrfHash) {
|
||||
if (csrfNameEl) csrfNameEl.value = res.csrfTokenName;
|
||||
if (csrfValueEl) csrfValueEl.value = res.csrfHash;
|
||||
}
|
||||
|
||||
if (!res || !res.ok) {
|
||||
if (selectEl && previousValue) selectEl.value = previousValue;
|
||||
msgEl.textContent = res && res.message ? res.message : 'Candidate assignment could not be saved.';
|
||||
return;
|
||||
}
|
||||
|
||||
msgEl.textContent = res.message || 'Candidate assignment saved.';
|
||||
if (selectEl) selectEl.dataset.previousValue = String(classSectionId);
|
||||
ensureIncludedClassVisible(classId);
|
||||
loadTotals();
|
||||
})
|
||||
.catch(() => {
|
||||
if (selectEl && previousValue) selectEl.value = previousValue;
|
||||
msgEl.textContent = 'Candidate assignment could not be saved.';
|
||||
})
|
||||
.finally(() => {
|
||||
if (selectEl) selectEl.disabled = false;
|
||||
});
|
||||
}
|
||||
|
||||
function loadTotals() {
|
||||
@@ -355,7 +914,6 @@
|
||||
.then(res => {
|
||||
if (!res || !res.ok) { msgEl.textContent = res && res.message ? res.message : 'Failed to load totals.'; return; }
|
||||
buildInitialTable(res.rows || []);
|
||||
updateNeeds();
|
||||
msgEl.textContent = '';
|
||||
})
|
||||
.catch(() => { msgEl.textContent = 'Failed to load totals.'; });
|
||||
@@ -402,6 +960,9 @@
|
||||
});
|
||||
|
||||
refreshBtn.addEventListener('click', function(){ loadTotals(); });
|
||||
if (studentSearchInput) {
|
||||
studentSearchInput.addEventListener('input', applyStudentSearch);
|
||||
}
|
||||
document.getElementById('generateAllBtn').addEventListener('click', async function(){
|
||||
const sectionCount = parseInt(sectionCountInput.value || '0', 10);
|
||||
const minStudents = parseInt(minInput.value || '0', 10);
|
||||
@@ -421,8 +982,6 @@
|
||||
}
|
||||
msgEl.textContent = 'All distributions completed.';
|
||||
});
|
||||
sectionCountInput.addEventListener('input', function(){ updateNeeds(); });
|
||||
|
||||
loadTotals();
|
||||
})();
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user