fix distribution page
Tests / PHPUnit (push) Successful in 1m17s

This commit is contained in:
root
2026-07-31 19:49:49 -04:00
parent 10dc8a33b4
commit b6f3b14e7b
3 changed files with 368 additions and 50 deletions
@@ -1,4 +1,69 @@
<?= $this->extend('layout/management_layout') ?>
<?= $this->section('styles') ?>
<style>
#classesTable {
table-layout: fixed;
}
#classesTable th:nth-child(1),
#classesTable td:nth-child(1) {
width: 10rem;
}
#classesTable th:nth-child(2),
#classesTable td:nth-child(2),
#classesTable th:nth-child(3),
#classesTable td:nth-child(3) {
width: 6rem;
}
#classesTable th:nth-child(4),
#classesTable td:nth-child(4) {
width: 10rem;
}
.distribution-cell {
min-width: 28rem;
}
.distribution-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr));
gap: .5rem;
}
.distribution-section {
border: 1px solid #dee2e6;
border-radius: .375rem;
background: #fff;
padding: .5rem;
}
.distribution-section-title {
display: flex;
align-items: center;
justify-content: space-between;
gap: .5rem;
margin-bottom: .25rem;
}
.distribution-meta {
display: flex;
flex-wrap: wrap;
gap: .25rem;
margin-bottom: .35rem;
}
.distribution-meta .badge {
font-weight: 500;
}
.distribution-students {
max-height: 8.5rem;
overflow: auto;
margin: 0;
padding-left: 1.1rem;
font-size: .8125rem;
line-height: 1.35;
}
.distribution-empty {
color: #6c757d;
font-size: .875rem;
}
</style>
<?= $this->endSection() ?>
<?= $this->section('content') ?>
<div class="container-fluid">
<div class="wrapper">
@@ -28,6 +93,32 @@
<span id="pageMsg" class="small text-muted"></span>
</div>
</div>
<div class="row g-3 align-items-end mb-3">
<div class="col-md-4">
<label for="additionalClassSelect" class="form-label">Additional Classes</label>
<select id="additionalClassSelect" class="form-select">
<option value="">Select class to include...</option>
<?php if (!empty($classes)): ?>
<?php foreach ($classes as $c): ?>
<?php
$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));
if (!$isBase || $isStandard) continue;
?>
<option value="<?= (int)($c['class_id'] ?? 0) ?>"><?= esc($name) ?></option>
<?php endforeach; ?>
<?php endif; ?>
</select>
</div>
<div class="col-md-2 d-grid">
<button type="button" id="addClassBtn" class="btn btn-outline-primary">Add Class</button>
</div>
<div class="col-md-6">
<div id="includedClasses" class="d-flex flex-wrap gap-2"></div>
</div>
</div>
<div class="table-responsive">
<table class="table table-sm table-striped align-middle no-mgmt-sticky" id="classesTable">
@@ -37,6 +128,7 @@
<th>Total</th>
<th>Sections</th>
<th>Actions</th>
<th>Distribution</th>
</tr>
</thead>
<tbody id="tblBody"></tbody>
@@ -57,50 +149,37 @@
<script>
(function(){
const selectedYear = '<?= esc($selectedYear ?? '') ?>';
const totalsUrl = '<?= site_url('administrator/sections/promotion-totals') ?>?school_year=' + encodeURIComponent(selectedYear);
const totalsBaseUrl = '<?= site_url('administrator/sections/promotion-totals') ?>';
const distUrl = '<?= site_url('administrator/sections/auto-distribute') ?>';
const tblBody = document.getElementById('tblBody');
const tblHeader = document.getElementById('tblHeader');
const sectionCountInput = document.getElementById('sectionCount');
const minInput = document.getElementById('minStudents');
const maxInput = document.getElementById('maxStudents');
const msgEl = document.getElementById('pageMsg');
const refreshBtn= document.getElementById('refreshTotalsBtn');
const additionalClassSelect = document.getElementById('additionalClassSelect');
const addClassBtn = document.getElementById('addClassBtn');
const includedClassesEl = document.getElementById('includedClasses');
let headerSections = []; // list of generated section names as columns
let rowIndexByClassId = {}; // mapping to locate rows
let includedClassIds = [];
function totalsUrl() {
const params = new URLSearchParams();
params.set('school_year', selectedYear);
if (includedClassIds.length) params.set('include_class_ids', includedClassIds.join(','));
return totalsBaseUrl + '?' + params.toString();
}
function selectedSectionCount() {
const count = parseInt(sectionCountInput.value || '0', 10);
return count > 0 ? count : '';
}
function ensureSectionColumns(sectionNames) {
// Add any new section name as a column if not already present
sectionNames.forEach(function(name){
if (!name) return;
if (headerSections.indexOf(name) >= 0) return;
headerSections.push(name);
const th = document.createElement('th');
th.textContent = name;
tblHeader.appendChild(th);
// Also add empty cell to each existing row
document.querySelectorAll('#tblBody tr').forEach(function(tr){
const td = document.createElement('td');
td.dataset.section = name;
td.textContent = '';
tr.appendChild(td);
});
});
}
function buildInitialTable(rows) {
tblBody.innerHTML = '';
rowIndexByClassId = {};
headerSections = [];
// Remove dynamically-added section cols, keep the first four fixed
while (tblHeader.children.length > 4) tblHeader.removeChild(tblHeader.lastElementChild);
rows.forEach(function(r){
const tr = document.createElement('tr');
@@ -130,13 +209,21 @@
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);
tr.appendChild(tdResults);
tblBody.appendChild(tr);
rowIndexByClassId[r.class_id] = tr;
});
rows.forEach(function(r){
if (Array.isArray(r.sections) && r.sections.length) {
renderSectionsForRow(r.class_section_name || '', r.sections);
renderSectionsForRow(r.class_section_id, r.sections);
}
});
}
@@ -186,42 +273,84 @@
msgEl.textContent = res && res.message ? res.message : 'Completed.';
renderSectionsForRow(baseName || '', res.sections);
renderSectionsForRow(baseSectionId, res.sections);
})
.catch(() => { msgEl.textContent = 'Failed to distribute. Please try again.'; });
}
function renderSectionsForRow(baseName, sections) {
const names = sections.map(s => s.class_section_name).filter(Boolean);
ensureSectionColumns(names);
function renderSectionsForRow(baseSectionId, sections) {
const tr = Array.from(document.querySelectorAll('#tblBody tr')).find(function(_tr){
return (_tr.children[0].textContent || '') === (baseName || '');
return String(_tr.dataset.classSectionId || '') === String(baseSectionId || '');
});
if (!tr) return;
const td = tr.querySelector('.distribution-cell');
if (!td) return;
td.innerHTML = '';
const grid = document.createElement('div');
grid.className = 'distribution-grid';
sections.forEach(function(s){
const td = Array.from(tr.querySelectorAll('td[data-section]')).find(cell => cell.dataset.section === s.class_section_name);
if (!td) return;
const studentNames = Array.isArray(s.student_names) ? s.student_names : [];
td.innerHTML = '';
const count = document.createElement('div');
count.className = 'fw-semibold small mb-1';
count.textContent = (s.total ?? studentNames.length) + ' students';
td.appendChild(count);
const block = document.createElement('div');
block.className = 'distribution-section';
const list = document.createElement('div');
list.className = 'small';
list.style.whiteSpace = 'normal';
list.textContent = studentNames.length ? studentNames.join(', ') : 'No students assigned';
td.appendChild(list);
const title = document.createElement('div');
title.className = 'distribution-section-title';
const name = document.createElement('div');
name.className = 'fw-semibold';
name.textContent = s.class_section_name || 'Section';
title.appendChild(name);
const count = document.createElement('div');
count.className = 'badge bg-primary';
count.textContent = (s.total ?? studentNames.length) + ' students';
title.appendChild(count);
block.appendChild(title);
const meta = document.createElement('div');
meta.className = 'distribution-meta';
if (Number.isFinite(Number(s.average_score))) {
const avg = document.createElement('span');
avg.className = 'badge text-bg-light';
avg.textContent = 'Avg ' + Number(s.average_score).toFixed(2);
meta.appendChild(avg);
}
if (Number.isFinite(Number(s.male)) || Number.isFinite(Number(s.female))) {
const gender = document.createElement('span');
gender.className = 'badge text-bg-light';
gender.textContent = 'M ' + (s.male ?? 0) + ' / F ' + (s.female ?? 0);
meta.appendChild(gender);
}
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);
} else {
const empty = document.createElement('div');
empty.className = 'distribution-empty';
empty.textContent = 'No students assigned';
block.appendChild(empty);
}
grid.appendChild(block);
});
td.appendChild(grid);
}
function loadTotals() {
msgEl.textContent = 'Loading totals...';
fetch(totalsUrl, { headers: { 'X-Requested-With': 'XMLHttpRequest' }})
fetch(totalsUrl(), { headers: { 'X-Requested-With': 'XMLHttpRequest' }})
.then(r => r.json())
.then(res => {
if (!res || !res.ok) { msgEl.textContent = res && res.message ? res.message : 'Failed to load totals.'; return; }
@@ -232,6 +361,46 @@
.catch(() => { msgEl.textContent = 'Failed to load totals.'; });
}
function renderIncludedClasses() {
includedClassesEl.innerHTML = '';
includedClassIds.forEach(function(classId){
const option = additionalClassSelect.querySelector('option[value="' + String(classId) + '"]');
const badge = document.createElement('span');
badge.className = 'badge bg-secondary d-inline-flex align-items-center gap-2';
const label = document.createElement('span');
label.textContent = option ? option.textContent : ('Class #' + classId);
badge.appendChild(label);
const removeBtn = document.createElement('button');
removeBtn.type = 'button';
removeBtn.className = 'btn-close btn-close-white';
removeBtn.setAttribute('aria-label', 'Remove class');
removeBtn.addEventListener('click', function(){
includedClassIds = includedClassIds.filter(id => id !== classId);
renderIncludedClasses();
loadTotals();
});
badge.appendChild(removeBtn);
includedClassesEl.appendChild(badge);
});
}
addClassBtn.addEventListener('click', function(){
const classId = parseInt(additionalClassSelect.value || '0', 10);
if (!classId) {
msgEl.textContent = 'Select a class to include.';
return;
}
if (includedClassIds.indexOf(classId) < 0) {
includedClassIds.push(classId);
renderIncludedClasses();
}
additionalClassSelect.value = '';
loadTotals();
});
refreshBtn.addEventListener('click', function(){ loadTotals(); });
document.getElementById('generateAllBtn').addEventListener('click', async function(){
const sectionCount = parseInt(sectionCountInput.value || '0', 10);