Files
alrahma_sunday_school/app/Views/administrator/sections_auto_distribute.php
T
2026-02-10 22:11:06 -05:00

240 lines
9.6 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">Auto-Distribute Students into Sections</h2>
<div class="row g-2 align-items-center justify-content-center mb-3">
<div class="col-auto"><label for="adYearSelect" class="col-form-label">School year</label></div>
<div class="col-auto">
<form method="get" action="<?= site_url('administrator/sections/auto-distribute') ?>" class="d-flex align-items-center gap-2">
<select id="adYearSelect" 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>
<button type="submit" class="btn btn-secondary btn-sm">Apply</button>
</form>
</div>
</div>
<div class="card">
<div class="card-body">
<div class="row g-3 align-items-end mb-2">
<div class="col-md-3">
<label for="globalPer" class="form-label">Students per section</label>
<input type="number" min="1" id="globalPer" class="form-control" placeholder="e.g. 20" />
</div>
<div class="col-md-4 d-flex gap-2">
<button type="button" id="refreshTotalsBtn" class="btn btn-outline-secondary">Refresh Totals</button>
<button type="button" id="generateAllBtn" class="btn btn-primary">Generate All</button>
</div>
<div class="col-md-7 text-end">
<span id="pageMsg" class="small text-muted"></span>
</div>
</div>
<div class="table-responsive">
<table class="table table-sm table-striped align-middle no-mgmt-sticky" id="classesTable">
<thead class="table-light">
<tr id="tblHeader">
<th>Class</th>
<th>Total</th>
<th>Sections Needed</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="tblBody"></tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
<?= $this->endSection() ?>
<?= $this->section('scripts') ?>
<input type="hidden" id="csrfName" value="<?= esc(csrf_token()) ?>" />
<input type="hidden" id="csrfValue" value="<?= esc(csrf_hash()) ?>" />
<script>
(function(){
const selectedYear = '<?= esc($selectedYear ?? '') ?>';
const totalsUrl = '<?= site_url('administrator/sections/promotion-totals') ?>?school_year=' + encodeURIComponent(selectedYear);
const distUrl = '<?= site_url('administrator/sections/auto-distribute') ?>';
const tblBody = document.getElementById('tblBody');
const tblHeader = document.getElementById('tblHeader');
const perInput = document.getElementById('globalPer');
const msgEl = document.getElementById('pageMsg');
const refreshBtn= document.getElementById('refreshTotalsBtn');
let headerSections = []; // list of generated section names as columns
let rowIndexByClassId = {}; // mapping to locate rows
function calcNeeded(total) {
const per = parseInt(perInput.value || '0', 10);
if (!per || per <= 0) return '';
return Math.ceil(total / per);
}
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');
tr.dataset.classId = r.class_id;
tr.dataset.classSectionId = r.class_section_id;
tr.dataset.className = r.class_section_name || '';
const tdName = document.createElement('td');
tdName.textContent = r.class_section_name || '';
tr.appendChild(tdName);
const tdTotal = document.createElement('td');
tdTotal.className = 'text-end';
tdTotal.textContent = r.total;
tr.appendChild(tdTotal);
const tdNeed = document.createElement('td');
tdNeed.className = 'text-end need-cell';
tdNeed.textContent = calcNeeded(r.total);
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);
tblBody.appendChild(tr);
rowIndexByClassId[r.class_id] = tr;
});
}
function updateNeeds() {
document.querySelectorAll('#tblBody tr').forEach(function(tr){
const total = parseInt(tr.children[1].textContent || '0', 10);
const needCell = tr.querySelector('.need-cell');
if (needCell) needCell.textContent = calcNeeded(total);
});
}
function runDistribution(baseSectionId, baseName) {
const per = parseInt(perInput.value || '0', 10);
if (!per || per <= 0) { msgEl.textContent = 'Enter students per section first.'; return; }
msgEl.textContent = 'Distributing ' + (baseName || '') + '...';
const fd = new FormData();
fd.append('class_section_id', String(baseSectionId));
fd.append('students_per_section', String(per));
fd.append('school_year', selectedYear);
// CSRF
const csrfNameEl = document.getElementById('csrfName');
const csrfValueEl= document.getElementById('csrfValue');
if (csrfNameEl && csrfValueEl) {
fd.append(csrfNameEl.value, csrfValueEl.value);
}
return fetch(distUrl, { method: 'POST', headers: { 'X-Requested-With': 'XMLHttpRequest' }, body: fd })
.then(r => r.json())
.then(res => {
// refresh CSRF if provided
if (res && res.csrfTokenName && res.csrfHash) {
if (csrfNameEl) csrfNameEl.value = res.csrfTokenName;
if (csrfValueEl) csrfValueEl.value = res.csrfHash;
}
if (!res || !res.ok || !Array.isArray(res.sections)) {
msgEl.textContent = res && res.message ? res.message : 'Distribution finished with no sections.';
return;
}
msgEl.textContent = res && res.message ? res.message : 'Completed.';
// Collect section names, then ensure header columns
const names = res.sections.map(s => s.class_section_name).filter(Boolean);
ensureSectionColumns(names);
// Fill row cells for this class
const tr = Array.from(document.querySelectorAll('#tblBody tr')).find(function(_tr){
return (_tr.children[0].textContent || '') === (baseName || '');
});
if (!tr) return;
res.sections.forEach(function(s){
const td = tr.querySelector('td[data-section="'+ s.class_section_name +'"]');
if (td) td.textContent = (s.total ?? 0) + ' (M'+ (s.male ?? 0) + '/F'+ (s.female ?? 0) +')';
});
})
.catch(() => { msgEl.textContent = 'Failed to distribute. Please try again.'; });
}
function loadTotals() {
msgEl.textContent = 'Loading totals...';
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; }
buildInitialTable(res.rows || []);
updateNeeds();
msgEl.textContent = '';
})
.catch(() => { msgEl.textContent = 'Failed to load totals.'; });
}
refreshBtn.addEventListener('click', function(){ updateNeeds(); });
document.getElementById('generateAllBtn').addEventListener('click', async function(){
const per = parseInt(perInput.value || '0', 10);
if (!per || per <= 0) { msgEl.textContent = 'Enter students per section first.'; return; }
// Collect base sections from rows
const rows = Array.from(document.querySelectorAll('#tblBody tr'))
.map(tr => ({ id: tr.dataset.classSectionId, name: tr.dataset.className }))
.filter(r => r.id);
for (let i = 0; i < rows.length; i++) {
const r = rows[i];
msgEl.textContent = 'Distributing ' + (r.name || r.id) + ' (' + (i+1) + '/' + rows.length + ')...';
// eslint-disable-next-line no-await-in-loop
await runDistribution(r.id, r.name);
}
msgEl.textContent = 'All distributions completed.';
});
perInput.addEventListener('input', function(){ updateNeeds(); });
loadTotals();
})();
</script>
<?= $this->endSection() ?>