recreate project
This commit is contained in:
@@ -0,0 +1,353 @@
|
||||
<?= $this->extend('layout/management_layout') ?>
|
||||
<?= $this->section('content') ?>
|
||||
<?php
|
||||
// Role formatter for display + posting
|
||||
$formatRole = function (?string $role): string {
|
||||
$role = (string)$role;
|
||||
$role = str_replace(['-', '_'], ' ', $role);
|
||||
$role = preg_replace('/\s+/', ' ', trim($role));
|
||||
if ($role === '') return '';
|
||||
$out = [];
|
||||
foreach (explode(' ', $role) as $w) {
|
||||
if ($w === '') continue;
|
||||
if (preg_match('/^[A-Za-z]{1,3}$/', $w)) {
|
||||
$out[] = strtoupper($w); // TA, PTA, HR, KG...
|
||||
} else {
|
||||
$out[] = ucfirst(strtolower($w)); // Teacher, Assistant, Admin...
|
||||
}
|
||||
}
|
||||
return implode(' ', $out);
|
||||
};
|
||||
?>
|
||||
<div class="container-fluid">
|
||||
<div class="wrapper">
|
||||
<div class="content">
|
||||
|
||||
<div class="text-center mx-auto mb-5 wow" data-wow-delay="0.1s" style="max-width: 600px;">
|
||||
<br>
|
||||
<h2 class="text-dark mb-3" style="font-family: Arial, sans-serif;">Generate Staff Badges</h2>
|
||||
</div>
|
||||
|
||||
<?= $this->include('partials/academic_filter') ?>
|
||||
|
||||
<div class="d-flex flex-column flex-md-row justify-content-md-end align-items-md-center gap-2">
|
||||
<!-- Submit button associates to form below -->
|
||||
<button type="submit" class="btn btn-success mt-3" form="badgeForm">Generate Badges</button>
|
||||
<br>
|
||||
</div>
|
||||
<!-- Staff selection -->
|
||||
<form method="post" action="<?= base_url('badge') ?>" target="_blank" id="badgeForm">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="school_year" value="<?= esc($selectedYear ?? '') ?>">
|
||||
<!-- hidden container to preserve selections and their data (guaranteed inside form) -->
|
||||
<div id="selectedHiddenInputs" style="display:none"></div>
|
||||
|
||||
<table id="staffTable" class="table table-bordered table-striped align-middle w-100">
|
||||
<thead class="table-dark">
|
||||
<tr>
|
||||
<th style="width: 70px;">#</th>
|
||||
<th scope="col">Firstname</th>
|
||||
<th scope="col">Lastname</th>
|
||||
<th scope="col">Role</th>
|
||||
<th scope="col">Teacher Class Section</th>
|
||||
<th scope="col" style="width: 140px;">Badge Prints</th>
|
||||
<th scope="col" style="width: 150px;">
|
||||
<div class="form-check m-0">
|
||||
<input class="form-check-input" type="checkbox" id="select-all">
|
||||
<label class="form-check-label" for="select-all">Select All (page)</label>
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (!empty($users)): ?>
|
||||
<?php $order = 1; ?>
|
||||
<?php foreach ($users as $user): ?>
|
||||
<?php
|
||||
// Pick a representative role (first of CSV or role_name/active_role)
|
||||
$roleRaw = $user['active_role'] ?? $user['role_name'] ?? ($user['roles'] ?? '');
|
||||
if (strpos((string)$roleRaw, ',') !== false) {
|
||||
$parts = array_filter(array_map('trim', explode(',', (string)$roleRaw)));
|
||||
$roleRaw = $parts[0] ?? '';
|
||||
}
|
||||
//$roleLabel = $roleRaw !== '' ? $formatRole($roleRaw) : '-';
|
||||
$roleLabel = $user['role_name'] ?? ($user['roles'] ?? '-'); // both are pre-formatted by $formatRole
|
||||
|
||||
// Normalize user id key for checkbox
|
||||
$uid = $user['user_id'] ?? $user['id'] ?? ($user['users.id'] ?? null);
|
||||
|
||||
// Class section name (optional)
|
||||
$className = !empty($user['class_section_name']) ? (string)$user['class_section_name'] : '';
|
||||
?>
|
||||
<tr
|
||||
data-user-id="<?= esc($uid ?? '') ?>"
|
||||
data-role-label="<?= esc($roleLabel) ?>"
|
||||
data-class-name="<?= esc($className) ?>">
|
||||
<td style="text-align: center;"><?= esc($order++) ?></td>
|
||||
<td><?= esc($user['firstname'] ?? '') ?></td>
|
||||
<td><?= esc($user['lastname'] ?? '') ?></td>
|
||||
<td><?= esc($roleLabel) ?></td>
|
||||
<td><?= $className !== '' ? esc($className) : '-' ?></td>
|
||||
<td><span class="badge bg-secondary prints-badge" data-user-id="<?= esc($uid ?? '') ?>">—</span></td>
|
||||
<td>
|
||||
<?php if ($uid !== null): ?>
|
||||
<div class="form-check m-0">
|
||||
<input type="checkbox" name="user_ids[]" value="<?= esc($uid) ?>" class="form-check-input user-checkbox" id="chk-<?= esc($uid) ?>">
|
||||
<label class="form-check-label" for="chk-<?= esc($uid) ?>">Select</label>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<span class="text-muted">N/A</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php else: ?>
|
||||
<tr>
|
||||
<td colspan="7" class="text-center text-muted">No staff found for the selected year.</td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</form>
|
||||
<br>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
<?= $this->section('scripts') ?>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Track selected user IDs across paging/filtering/sorting
|
||||
const selected = new Set();
|
||||
const formEl = document.getElementById('badgeForm');
|
||||
let clearTimerId = null; // pending auto-clear timer, if any
|
||||
let hiddenContainer = document.getElementById('selectedHiddenInputs');
|
||||
// Ensure hidden container exists inside the form (some earlier layouts had it outside)
|
||||
if (!hiddenContainer || !formEl.contains(hiddenContainer)) {
|
||||
hiddenContainer = document.createElement('div');
|
||||
hiddenContainer.id = 'selectedHiddenInputs';
|
||||
hiddenContainer.style.display = 'none';
|
||||
formEl.appendChild(hiddenContainer);
|
||||
}
|
||||
const selectAll = document.getElementById('select-all');
|
||||
const csrfName = <?= json_encode(csrf_token()) ?>;
|
||||
const csrfInput = document.querySelector(`input[name='${csrfName}']`);
|
||||
|
||||
// Build a meta map (userId -> { role, className }) from the DOM BEFORE DataTables paginates
|
||||
const rowMeta = {};
|
||||
document.querySelectorAll('#staffTable tbody tr').forEach(tr => {
|
||||
const id = tr.getAttribute('data-user-id');
|
||||
if (!id) return;
|
||||
rowMeta[id] = {
|
||||
role: tr.getAttribute('data-role-label') || '',
|
||||
className: tr.getAttribute('data-class-name') || ''
|
||||
};
|
||||
});
|
||||
|
||||
const table = $('#staffTable').DataTable({
|
||||
stateSave: true,
|
||||
pageLength: 100,
|
||||
lengthMenu: [10, 25, 50, 100],
|
||||
order: [
|
||||
[1, 'asc'],
|
||||
[2, 'asc']
|
||||
], // Firstname, Lastname
|
||||
columnDefs: [{
|
||||
targets: 0,
|
||||
searchable: false
|
||||
}, // row #
|
||||
{
|
||||
targets: 6,
|
||||
orderable: false,
|
||||
searchable: false
|
||||
} // checkbox column
|
||||
]
|
||||
});
|
||||
|
||||
// Helper: keep hidden inputs in sync so selections submit even if not on current page
|
||||
function syncHiddenInputs() {
|
||||
hiddenContainer.innerHTML = '';
|
||||
selected.forEach(function(id) {
|
||||
const meta = rowMeta[id] || {
|
||||
role: '',
|
||||
className: ''
|
||||
};
|
||||
// user_ids[]
|
||||
const i1 = document.createElement('input');
|
||||
i1.type = 'hidden';
|
||||
i1.name = 'user_ids[]';
|
||||
i1.value = id;
|
||||
hiddenContainer.appendChild(i1);
|
||||
// roles[ID]
|
||||
const i2 = document.createElement('input');
|
||||
i2.type = 'hidden';
|
||||
i2.name = `roles[${id}]`;
|
||||
i2.value = meta.role;
|
||||
hiddenContainer.appendChild(i2);
|
||||
// classes[ID]
|
||||
const i3 = document.createElement('input');
|
||||
i3.type = 'hidden';
|
||||
i3.name = `classes[${id}]`;
|
||||
i3.value = meta.className;
|
||||
hiddenContainer.appendChild(i3);
|
||||
});
|
||||
}
|
||||
|
||||
// Helper: refresh checkboxes on current page based on Set
|
||||
function syncPageCheckboxes() {
|
||||
const pageNodes = table.rows({
|
||||
page: 'current'
|
||||
}).nodes().to$();
|
||||
let allChecked = true;
|
||||
let anyChecked = false;
|
||||
pageNodes.each(function() {
|
||||
const row = this;
|
||||
const userId = row.getAttribute('data-user-id');
|
||||
const cb = row.querySelector('.user-checkbox');
|
||||
if (!cb || !userId) return;
|
||||
cb.checked = selected.has(userId);
|
||||
if (!cb.checked) allChecked = false;
|
||||
if (cb.checked) anyChecked = true;
|
||||
});
|
||||
// Update header select-all for current page
|
||||
selectAll.checked = allChecked && pageNodes.length > 0;
|
||||
selectAll.indeterminate = !allChecked && anyChecked;
|
||||
}
|
||||
|
||||
// Helper: clear all selections and reset the current page UI
|
||||
function clearSelectionsNow() {
|
||||
selected.clear();
|
||||
// Uncheck visible checkboxes on current page
|
||||
table.rows({ page: 'current' }).every(function () {
|
||||
const row = this.node();
|
||||
const cb = row.querySelector('.user-checkbox');
|
||||
if (cb) cb.checked = false;
|
||||
});
|
||||
// Reset select-all and hidden inputs
|
||||
selectAll.checked = false;
|
||||
selectAll.indeterminate = false;
|
||||
syncHiddenInputs();
|
||||
syncPageCheckboxes();
|
||||
}
|
||||
|
||||
// On draw (paging/sort/search), just refresh checkbox UI states
|
||||
table.on('draw', function() {
|
||||
syncPageCheckboxes();
|
||||
});
|
||||
|
||||
// Delegate checkbox change handling once at the table level (works across redraws)
|
||||
document.getElementById('staffTable').addEventListener('change', function (e) {
|
||||
const target = e.target;
|
||||
if (!target || !target.classList.contains('user-checkbox')) return;
|
||||
const tr = target.closest('tr[data-user-id]');
|
||||
const userId = tr ? tr.getAttribute('data-user-id') : null;
|
||||
if (!userId) return;
|
||||
if (target.checked) selected.add(userId); else selected.delete(userId);
|
||||
syncHiddenInputs();
|
||||
syncPageCheckboxes();
|
||||
// If user interacts, cancel any pending auto-clear to avoid wiping new selections
|
||||
if (clearTimerId !== null) { clearTimeout(clearTimerId); clearTimerId = null; }
|
||||
});
|
||||
|
||||
// Initial sync on first load
|
||||
table.draw(false);
|
||||
|
||||
// Header "Select All (page)" toggler
|
||||
selectAll.addEventListener('change', function() {
|
||||
const check = this.checked;
|
||||
table.rows({
|
||||
page: 'current'
|
||||
}).every(function() {
|
||||
const row = this.node();
|
||||
const userId = row.getAttribute('data-user-id');
|
||||
const cb = row.querySelector('.user-checkbox');
|
||||
if (!cb || !userId) return;
|
||||
cb.checked = check;
|
||||
if (check) selected.add(userId);
|
||||
else selected.delete(userId);
|
||||
});
|
||||
syncHiddenInputs();
|
||||
syncPageCheckboxes();
|
||||
if (clearTimerId !== null) { clearTimeout(clearTimerId); clearTimerId = null; }
|
||||
});
|
||||
|
||||
// Before submit, ensure CSRF is fresh and hidden inputs include all selections
|
||||
const form = document.getElementById('badgeForm');
|
||||
async function refreshCsrf() {
|
||||
try {
|
||||
const params = new URLSearchParams();
|
||||
if (selectedYear) params.append('school_year', selectedYear);
|
||||
const resp = await fetch('<?= base_url('api/printables/badges/status') ?>' + (params.toString() ? ('?' + params.toString()) : ''), {
|
||||
method: 'GET',
|
||||
headers: { 'Accept': 'application/json' }
|
||||
});
|
||||
if (!resp.ok) return;
|
||||
const json = await resp.json();
|
||||
if (json && json.csrf_token && json.csrf_hash && csrfInput && json.csrf_token === csrfName) {
|
||||
csrfInput.value = json.csrf_hash;
|
||||
}
|
||||
} catch (e) {
|
||||
// No-op: if refresh fails we'll still attempt submit; server may accept existing token
|
||||
}
|
||||
}
|
||||
|
||||
form.addEventListener('submit', async function(e) {
|
||||
e.preventDefault();
|
||||
if (selected.size === 0) {
|
||||
alert('Please select at least one staff member to generate badges.');
|
||||
return;
|
||||
}
|
||||
syncHiddenInputs();
|
||||
// CSRF is excluded for this endpoint now, but keeping refresh is safe
|
||||
try { await refreshCsrf(); } catch (_) {}
|
||||
// Native submit so browser handles PDF in a new tab
|
||||
form.submit();
|
||||
// Auto-clear selections 5 seconds after generating badges
|
||||
clearTimerId = setTimeout(function() { clearSelectionsNow(); clearTimerId = null; }, 5000);
|
||||
});
|
||||
|
||||
// --- Fetch and render print status ---
|
||||
const selectedYear = <?= json_encode($selectedYear ?? '') ?>;
|
||||
|
||||
function fetchPrintStatus() {
|
||||
const ids = Object.keys(rowMeta);
|
||||
if (ids.length === 0) return;
|
||||
const params = new URLSearchParams();
|
||||
ids.forEach(id => params.append('user_ids[]', id));
|
||||
if (selectedYear) params.append('school_year', selectedYear);
|
||||
|
||||
fetch('<?= base_url('api/printables/badges/status') ?>?' + params.toString(), {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
})
|
||||
.then(r => r.ok ? r.json() : Promise.reject())
|
||||
.then(json => {
|
||||
if (!json || !json.ok || !json.data) return;
|
||||
const data = json.data;
|
||||
Object.keys(rowMeta).forEach(id => {
|
||||
const info = data[id];
|
||||
const badge = document.querySelector(`.prints-badge[data-user-id="${id}"]`);
|
||||
const tr = document.querySelector(`#staffTable tbody tr[data-user-id="${id}"]`);
|
||||
if (!badge || !tr) return;
|
||||
const count = info ? (info.count || 0) : 0;
|
||||
badge.textContent = count > 0 ? `Printed ${count}` : '—';
|
||||
badge.className = 'badge prints-badge ' + (count > 0 ? 'bg-success' : 'bg-secondary');
|
||||
tr.classList.toggle('table-success', count > 0);
|
||||
});
|
||||
// Refresh CSRF for subsequent submissions (so no page reload is needed)
|
||||
if (json.csrf_token && json.csrf_hash && csrfInput && json.csrf_token === csrfName) {
|
||||
csrfInput.value = json.csrf_hash;
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
fetchPrintStatus();
|
||||
window.addEventListener('focus', fetchPrintStatus);
|
||||
});
|
||||
</script>
|
||||
<?= $this->endSection() ?>
|
||||
@@ -0,0 +1,302 @@
|
||||
<?= $this->extend('layout/management_layout') ?>
|
||||
<?= $this->section('content') ?>
|
||||
<div class="container-fluid">
|
||||
<div class="wrapper">
|
||||
<div class="content"></div>
|
||||
<div class="text-center mx-auto mb-4" style="max-width: 700px;">
|
||||
<h2 class="text-dark" style="font-family: Arial, sans-serif;">Report Card Management</h2>
|
||||
</div>
|
||||
|
||||
<div class="row g-2 align-items-center justify-content-center mb-3">
|
||||
<div class="col-auto"><label for="schoolYearSelect" class="col-form-label">School year</label></div>
|
||||
<div class="col-auto">
|
||||
<select id="schoolYearSelect" class="form-select form-select-sm" style="min-width: 200px;"></select>
|
||||
</div>
|
||||
<div class="col-auto"><label for="semesterSelect" class="col-form-label">Semester</label></div>
|
||||
<div class="col-auto">
|
||||
<select id="semesterSelect" class="form-select form-select-sm" style="min-width: 160px;"></select>
|
||||
</div>
|
||||
<div class="col-auto"><label for="reportDate" class="col-form-label">Report Date</label></div>
|
||||
<div class="col-auto">
|
||||
<input type="date" id="reportDate" class="form-control form-control-sm" style="min-width: 170px;" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form id="classForm" class="mb-3" onsubmit="return false;">
|
||||
<div class="row align-items-end justify-content-center g-2">
|
||||
<div class="col-md-4 mb-2">
|
||||
<label for="class_section_id">Select Class Section</label>
|
||||
<select class="form-control" id="class_section_id"></select>
|
||||
</div>
|
||||
<div class="col-md-4 d-flex gap-2 mb-2 justify-content-center">
|
||||
<button type="button" class="btn btn-primary" id="viewClassBtn">View All Reports</button>
|
||||
<a href="#" class="btn btn-secondary external-link" id="printClassBtn" target="_blank">Print</a>
|
||||
<a id="downloadClassBtn" href="#" class="btn btn-success external-link" target="_blank">Download</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<form id="studentForm" class="mb-3" onsubmit="return false;">
|
||||
<div class="row align-items-end justify-content-center g-2">
|
||||
<div class="col-md-4 mb-2">
|
||||
<label for="student_id">Select Student</label>
|
||||
<select class="form-control" id="student_id"></select>
|
||||
</div>
|
||||
<div class="col-md-4 d-flex gap-2 mb-2 justify-content-center">
|
||||
<button type="button" class="btn btn-primary" id="viewStudentBtn">View Report</button>
|
||||
<a href="#" class="btn btn-secondary external-link" id="printStudentBtn" target="_blank">Print</a>
|
||||
<a id="downloadStudentBtn" href="#" class="btn btn-success external-link" target="_blank">Download</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="card shadow-sm mb-3">
|
||||
<div class="card-body">
|
||||
<div class="d-flex flex-wrap gap-2 align-items-center justify-content-between">
|
||||
<div>
|
||||
<h5 class="mb-1">Report Card Completeness</h5>
|
||||
<div class="text-muted small">Check grades, comments, and final scores for the selected class.</div>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<button type="button" class="btn btn-outline-primary btn-sm" id="checkCompletenessBtn">Check Completeness</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="completenessStatus" class="text-muted small mt-2"></div>
|
||||
<div id="completenessSummary" class="mt-2"></div>
|
||||
<div id="completenessTableWrap" class="table-responsive mt-3" hidden>
|
||||
<table class="table table-sm table-bordered align-middle mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th style="width: 35%;">Student</th>
|
||||
<th style="width: 15%;">Status</th>
|
||||
<th>Missing</th>
|
||||
<th>Warnings</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="completenessBody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<iframe id="studentReportFrame" style="width:100%; height:600px; border:1px solid #ccc;" hidden></iframe>
|
||||
<iframe id="classReportFrame" style="width:100%; height:600px; border:1px solid #ccc;" hidden></iframe>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
<?= $this->section('scripts') ?>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function(){
|
||||
const API = <?= json_encode(site_url('api/printables/report-card/meta')) ?>;
|
||||
const COMPLETENESS_API = <?= json_encode(site_url('api/printables/report-card/completeness')) ?>;
|
||||
|
||||
// add cache-busting cb param
|
||||
const withCB = (u) => u + (u.includes('?') ? '&' : '?') + 'cb=' + Date.now();
|
||||
|
||||
const urlStudent = (id, y, s, d=false, rd=null) =>
|
||||
withCB(`<?= site_url('report-card/student') ?>/${encodeURIComponent(id)}?school_year=${encodeURIComponent(y)}${s?('&semester='+encodeURIComponent(s)) : ''}${d?'&download=1':''}${rd?('&report_date='+encodeURIComponent(rd)) : ''}`);
|
||||
|
||||
const urlClass = (cid, y, s, d=false, rd=null) =>
|
||||
withCB(`<?= site_url('report-card/class') ?>/${encodeURIComponent(cid)}?school_year=${encodeURIComponent(y)}${s?('&semester='+encodeURIComponent(s)) : ''}${d?'&download=1':''}${rd?('&report_date='+encodeURIComponent(rd)) : ''}`);
|
||||
|
||||
const $year = document.getElementById('schoolYearSelect');
|
||||
const $student= document.getElementById('student_id');
|
||||
const $class = document.getElementById('class_section_id');
|
||||
const $sem = document.getElementById('semesterSelect');
|
||||
const $rdate = document.getElementById('reportDate');
|
||||
const $viewS = document.getElementById('viewStudentBtn');
|
||||
const $viewC = document.getElementById('viewClassBtn');
|
||||
const $printS = document.getElementById('printStudentBtn');
|
||||
const $dlS = document.getElementById('downloadStudentBtn');
|
||||
const $printC = document.getElementById('printClassBtn');
|
||||
const $dlC = document.getElementById('downloadClassBtn');
|
||||
const $iframeS= document.getElementById('studentReportFrame');
|
||||
const $iframeC= document.getElementById('classReportFrame');
|
||||
const $checkCompleteness = document.getElementById('checkCompletenessBtn');
|
||||
const $completenessStatus = document.getElementById('completenessStatus');
|
||||
const $completenessSummary = document.getElementById('completenessSummary');
|
||||
const $completenessWrap = document.getElementById('completenessTableWrap');
|
||||
const $completenessBody = document.getElementById('completenessBody');
|
||||
|
||||
function opt(el, val, label) {
|
||||
const o = document.createElement('option'); o.value = String(val); o.textContent = label; el.appendChild(o);
|
||||
}
|
||||
|
||||
async function loadMeta(year=null, sem=null){
|
||||
let url = year ? (API + '?school_year=' + encodeURIComponent(year)) : API;
|
||||
if (sem) url += (url.indexOf('?')>-1?'&':'?') + 'semester=' + encodeURIComponent(sem);
|
||||
const res = await fetch(url, { credentials: 'same-origin' });
|
||||
const data = await res.json();
|
||||
if (!res.ok || data?.ok === false) throw new Error('Failed to load data');
|
||||
|
||||
// Years
|
||||
$year.innerHTML = '';
|
||||
(data.schoolYears || []).forEach(y => opt($year, y, y));
|
||||
if (data.selectedYear) $year.value = data.selectedYear;
|
||||
|
||||
// Semesters
|
||||
$sem.innerHTML = '';
|
||||
(data.semesters || []).forEach(s => opt($sem, s, s));
|
||||
if (data.selectedSemester) $sem.value = data.selectedSemester;
|
||||
|
||||
// Students
|
||||
$student.innerHTML = '';
|
||||
const students = (data.students || []);
|
||||
if (students.length === 0) {
|
||||
opt($student, '', 'No students found');
|
||||
} else {
|
||||
students.forEach(s => {
|
||||
const classLabel = (s.class_section_name || '').trim();
|
||||
const suffix = classLabel ? ` — ${classLabel}` : '';
|
||||
opt($student, s.id, `${s.firstname} ${s.lastname}${suffix}`);
|
||||
});
|
||||
}
|
||||
|
||||
// Classes
|
||||
$class.innerHTML = '';
|
||||
(data.classSections || []).forEach(c => opt($class, c.class_section_id || c.id, c.class_section_name));
|
||||
}
|
||||
|
||||
$year.addEventListener('change', function(){ loadMeta(this.value, $sem.value).catch(console.error); });
|
||||
$sem.addEventListener('change', function(){ loadMeta($year.value, this.value).catch(console.error); });
|
||||
|
||||
$viewS.addEventListener('click', function(){
|
||||
const sid = $student.value; const y = $year.value; const s=$sem.value;
|
||||
const rd = $rdate.value;
|
||||
if (!sid || !y) return;
|
||||
$iframeS.removeAttribute('hidden');
|
||||
$iframeS.src = urlStudent(sid, y, s, false, rd);
|
||||
});
|
||||
|
||||
$viewC.addEventListener('click', function(){
|
||||
const cid = $class.value; const y = $year.value; const s=$sem.value;
|
||||
const rd = $rdate.value;
|
||||
if (!cid || !y) return;
|
||||
$iframeC.removeAttribute('hidden');
|
||||
$iframeC.src = urlClass(cid, y, s, false, rd);
|
||||
});
|
||||
|
||||
const openExternal = (el, href) => {
|
||||
if (!href) return;
|
||||
el.href = href;
|
||||
el.setAttribute('target', '_blank');
|
||||
window.open(href, '_blank', 'noopener');
|
||||
};
|
||||
|
||||
$printS.addEventListener('click', function(e){
|
||||
e.preventDefault();
|
||||
const sid=$student.value, y=$year.value, s=$sem.value;
|
||||
const rd = $rdate.value;
|
||||
if(!sid||!y) return;
|
||||
openExternal(this, urlStudent(sid,y,s,false,rd));
|
||||
});
|
||||
|
||||
$dlS.addEventListener('click', function(e){
|
||||
e.preventDefault();
|
||||
const sid=$student.value, y=$year.value, s=$sem.value;
|
||||
const rd = $rdate.value;
|
||||
if(!sid||!y) return;
|
||||
openExternal(this, urlStudent(sid,y,s,true,rd));
|
||||
});
|
||||
|
||||
$printC.addEventListener('click', function(e){
|
||||
e.preventDefault();
|
||||
const cid=$class.value, y=$year.value, s=$sem.value;
|
||||
const rd = $rdate.value;
|
||||
if(!cid||!y) return;
|
||||
openExternal(this, urlClass(cid,y,s,false,rd));
|
||||
});
|
||||
|
||||
$dlC.addEventListener('click', function(e){
|
||||
e.preventDefault();
|
||||
const cid=$class.value, y=$year.value, s=$sem.value;
|
||||
const rd = $rdate.value;
|
||||
if(!cid||!y) return;
|
||||
openExternal(this, urlClass(cid,y,s,true,rd));
|
||||
});
|
||||
|
||||
const setCompletenessStatus = (msg, isError=false) => {
|
||||
$completenessStatus.textContent = msg || '';
|
||||
$completenessStatus.classList.toggle('text-danger', !!isError);
|
||||
};
|
||||
|
||||
const renderCompleteness = (data) => {
|
||||
const students = Array.isArray(data?.students) ? data.students : [];
|
||||
$completenessBody.innerHTML = '';
|
||||
if (students.length === 0) {
|
||||
$completenessWrap.hidden = true;
|
||||
$completenessSummary.textContent = 'No students found for the selected class.';
|
||||
return;
|
||||
}
|
||||
|
||||
const summary = data?.summary || {};
|
||||
const total = summary.total ?? students.length;
|
||||
const complete = summary.complete ?? 0;
|
||||
const incomplete = summary.incomplete ?? (total - complete);
|
||||
const warnings = summary.warnings ?? 0;
|
||||
const note = data?.used_fallback ? ' (roster uses year-only assignments)' : '';
|
||||
$completenessSummary.textContent = `Complete: ${complete}/${total} | Incomplete: ${incomplete} | With warnings: ${warnings}${note}`;
|
||||
|
||||
students.forEach((row) => {
|
||||
const missing = Array.isArray(row.missing) ? row.missing : [];
|
||||
const warningsList = Array.isArray(row.warnings) ? row.warnings : [];
|
||||
const isComplete = !!row.complete;
|
||||
const status = isComplete ? (warningsList.length ? 'Complete (warnings)' : 'Complete') : 'Missing data';
|
||||
const tr = document.createElement('tr');
|
||||
if (!isComplete) tr.classList.add('table-warning');
|
||||
const tdName = document.createElement('td');
|
||||
tdName.textContent = row.name || 'N/A';
|
||||
const tdStatus = document.createElement('td');
|
||||
tdStatus.textContent = status;
|
||||
const tdMissing = document.createElement('td');
|
||||
tdMissing.textContent = missing.length ? missing.join(', ') : 'None';
|
||||
const tdWarnings = document.createElement('td');
|
||||
tdWarnings.textContent = warningsList.length ? warningsList.join(', ') : 'None';
|
||||
tr.appendChild(tdName);
|
||||
tr.appendChild(tdStatus);
|
||||
tr.appendChild(tdMissing);
|
||||
tr.appendChild(tdWarnings);
|
||||
$completenessBody.appendChild(tr);
|
||||
});
|
||||
$completenessWrap.hidden = false;
|
||||
};
|
||||
|
||||
$checkCompleteness.addEventListener('click', async function(){
|
||||
const cid = $class.value;
|
||||
const y = $year.value;
|
||||
const s = $sem.value;
|
||||
if (!cid || !y) {
|
||||
setCompletenessStatus('Select a class and school year first.', true);
|
||||
return;
|
||||
}
|
||||
setCompletenessStatus('Checking completeness...');
|
||||
$completenessSummary.textContent = '';
|
||||
$completenessWrap.hidden = true;
|
||||
$completenessBody.innerHTML = '';
|
||||
try {
|
||||
const url = withCB(`${COMPLETENESS_API}?class_section_id=${encodeURIComponent(cid)}&school_year=${encodeURIComponent(y)}${s?('&semester='+encodeURIComponent(s)) : ''}`);
|
||||
const res = await fetch(url, { credentials: 'same-origin' });
|
||||
const data = await res.json();
|
||||
if (!res.ok || data?.ok === false) {
|
||||
throw new Error(data?.message || 'Failed to load completeness report.');
|
||||
}
|
||||
setCompletenessStatus('');
|
||||
renderCompleteness(data);
|
||||
} catch (err) {
|
||||
setCompletenessStatus(err?.message || 'Failed to load completeness report.', true);
|
||||
}
|
||||
});
|
||||
|
||||
// Default report date to today unless already set
|
||||
if (!$rdate.value) {
|
||||
const today = new Date();
|
||||
const pad = (n) => String(n).padStart(2,'0');
|
||||
$rdate.value = `${today.getFullYear()}-${pad(today.getMonth()+1)}-${pad(today.getDate())}`;
|
||||
}
|
||||
|
||||
loadMeta('<?= esc($schoolYear ?? '') ?>','<?= esc($semester ?? '') ?>').catch(console.error);
|
||||
});
|
||||
</script>
|
||||
<?= $this->endSection() ?>
|
||||
@@ -0,0 +1,585 @@
|
||||
<?= $this->extend('layout/management_layout') ?>
|
||||
<?= $this->section('content') ?>
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="wrapper">
|
||||
<div class="content">
|
||||
<h2 class="mb-4">Generate Student Stickers</h2>
|
||||
<?= $this->include('partials/academic_filter') ?>
|
||||
|
||||
<div class="d-flex gap-2 mb-3">
|
||||
<!-- Auto-count & Print All -->
|
||||
<button id="btnAutoCountPrintAll" type="button" class="btn btn-primary">
|
||||
<i class="bi bi-calculator"></i> Auto-count & Print All
|
||||
</button>
|
||||
|
||||
<!-- Auto-count for selected class -->
|
||||
<button id="btnAutoCountClass" type="button" class="btn btn-outline-primary">
|
||||
<i class="bi bi-collection"></i> Auto-count Selected Class
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Hidden form used by "Auto-count" flow -->
|
||||
<form id="autoPrintForm" action="<?= base_url('stickers/generate') ?>" method="post" target="_blank" class="d-none">
|
||||
<?= csrf_field() ?>
|
||||
|
||||
<!-- Carry the same layout fields as the visible form -->
|
||||
<input type="hidden" name="sticker_size" id="ap_size">
|
||||
<input type="hidden" name="sticker_width" id="ap_w">
|
||||
<input type="hidden" name="sticker_height" id="ap_h">
|
||||
<input type="hidden" name="stickers_per_page" id="ap_ppg">
|
||||
|
||||
<input type="hidden" name="page_size" id="ap_page_size">
|
||||
<input type="hidden" name="orientation" id="ap_orientation">
|
||||
|
||||
<input type="hidden" name="margin_left" id="ap_ml">
|
||||
<input type="hidden" name="margin_top" id="ap_mt">
|
||||
<input type="hidden" name="margin_right" id="ap_mr">
|
||||
<input type="hidden" name="margin_bottom" id="ap_mb">
|
||||
<input type="hidden" name="gap_x" id="ap_gx">
|
||||
<input type="hidden" name="gap_y" id="ap_gy">
|
||||
|
||||
<!-- Decide print mode -->
|
||||
<input type="hidden" name="print_all" id="ap_print_all" value="1">
|
||||
<input type="hidden" name="class_id" id="ap_class_id" value="">
|
||||
<!-- copies[ID] are injected dynamically -->
|
||||
</form>
|
||||
|
||||
<!-- Totals preview modal -->
|
||||
<div class="modal fade" id="previewTotalsModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-sm modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h6 class="modal-title">Sticker Totals</h6>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div id="previewTotalsBody" class="small"></div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button id="btnProceedPrint" type="button" class="btn btn-success">Proceed to Print</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- MAIN FORM (posts to generator) -->
|
||||
<form method="post" action="<?= base_url('stickers/generate') ?>" target="_blank" id="stickerForm">
|
||||
<?= csrf_field() ?>
|
||||
|
||||
<!-- Sticker settings -->
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-md-4">
|
||||
<label class="form-label" for="sticker_size">Sticker Size</label>
|
||||
<select name="sticker_size" id="sticker_size" class="form-select" required>
|
||||
<option value="60x30">Xsmall — 100×24.5 mm</option>
|
||||
<option value="60x30">Small — 60×30 mm</option>
|
||||
<option value="102x34" selected>Medium — 102×34 mm</option>
|
||||
<option value="90x45">Large — 90×45 mm</option>
|
||||
<option value="custom">Custom (enter mm below)</option>
|
||||
</select>
|
||||
<div class="row g-2 mt-2" id="customSizeRow" style="display:none;">
|
||||
<div class="col-6">
|
||||
<input type="number" min="1" step="0.1" class="form-control" id="sticker_width" name="sticker_width" placeholder="Width (mm)">
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<input type="number" min="1" step="0.1" class="form-control" id="sticker_height" name="sticker_height" placeholder="Height (mm)">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-text">Choose your label’s physical size. Use “Custom” to type exact mm.</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<label class="form-label" for="stickers_per_page">Stickers per Page</label>
|
||||
<input type="number" min="1" max="200" value="20" class="form-control" id="stickers_per_page" name="stickers_per_page" required>
|
||||
<div class="form-text">Optional cap (e.g., Avery sheets). Grid still derives from size & margins.</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<label class="form-label" for="page_size">Paper / Orientation</label>
|
||||
<div class="input-group">
|
||||
<select name="page_size" id="page_size" class="form-select">
|
||||
<option value="A4" selected>A4</option>
|
||||
<option value="Letter">Letter</option>
|
||||
</select>
|
||||
<select name="orientation" id="orientation" class="form-select" style="max-width:140px;">
|
||||
<option value="P" selected>Portrait</option>
|
||||
<option value="L">Landscape</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-text">Paper size & print orientation.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Layout: margins & gaps -->
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Margins (mm)</label>
|
||||
<div class="row g-2">
|
||||
<div class="col-6 col-lg-3">
|
||||
<input type="number" min="0" step="0.1" class="form-control" id="margin_left" name="margin_left" value="3" placeholder="Left">
|
||||
<div class="form-text">Left</div>
|
||||
</div>
|
||||
<div class="col-6 col-lg-3">
|
||||
<input type="number" min="0" step="0.1" class="form-control" id="margin_top" name="margin_top" value="12" placeholder="Top">
|
||||
<div class="form-text">Top</div>
|
||||
</div>
|
||||
<div class="col-6 col-lg-3">
|
||||
<input type="number" min="0" step="0.1" class="form-control" id="margin_right" name="margin_right" value="3" placeholder="Right">
|
||||
<div class="form-text">Right</div>
|
||||
</div>
|
||||
<div class="col-6 col-lg-3">
|
||||
<input type="number" min="0" step="0.1" class="form-control" id="margin_bottom" name="margin_bottom" value="12" placeholder="Bottom">
|
||||
<div class="form-text">Bottom</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Gaps Between Stickers (mm)</label>
|
||||
<div class="row g-2">
|
||||
<div class="col-6">
|
||||
<input type="number" min="0" step="0.1" class="form-control" id="gap_x" name="gap_x" value="3" placeholder="Horizontal gap">
|
||||
<div class="form-text">Horizontal (X)</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<input type="number" min="0" step="0.1" class="form-control" id="gap_y" name="gap_y" value="3" placeholder="Vertical gap">
|
||||
<div class="form-text">Vertical (Y)</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Selectors -->
|
||||
<div class="row g-3 align-items-end">
|
||||
<!-- Class picker -->
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="class_id">Select a Class</label>
|
||||
<select name="class_id" id="class_id" class="form-select">
|
||||
<option value="">-- Select Class --</option>
|
||||
<?php foreach ($classes as $class): ?>
|
||||
<option value="<?= esc($class['class_section_id']) ?>">
|
||||
<?= esc($class['class_section_name']) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<div class="form-text">Pick a class to load its students below.</div>
|
||||
</div>
|
||||
|
||||
<!-- Single student picker -->
|
||||
<div class="col-md-3">
|
||||
<label class="form-label" for="student_id">Or Select a Specific Student (optional)</label>
|
||||
<select name="student_id" id="student_id" class="form-select">
|
||||
<option value="">-- None (generate by class) --</option>
|
||||
<?php foreach ($students as $student): ?>
|
||||
<option value="<?= esc($student['id']) ?>">
|
||||
<?= esc($student['firstname'] . ' ' . $student['lastname']) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<div class="form-text">Leave blank to generate stickers for the whole class.</div>
|
||||
</div>
|
||||
|
||||
<!-- Copies input -->
|
||||
<div class="col-md-3">
|
||||
<label class="form-label" for="single_copies">Copies (if a student is selected)</label>
|
||||
<input type="number" min="0" max="100" value="1" class="form-control" name="single_copies" id="single_copies">
|
||||
<div class="form-text">Set how many stickers to print.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4 d-flex gap-2 align-items-center">
|
||||
<button type="submit" class="btn btn-primary">Generate Stickers</button>
|
||||
|
||||
<!-- Optional helper to fill all quantities in the class table -->
|
||||
<div class="input-group" style="max-width:260px;">
|
||||
<span class="input-group-text">Set all Qty</span>
|
||||
<input type="number" min="0" max="100" value="1" id="fillAllQty" class="form-control">
|
||||
<button type="button" class="btn btn-outline-secondary" id="btnFillAll">Apply</button>
|
||||
</div>
|
||||
|
||||
<small class="text-muted ms-2" id="settingsSummary"></small>
|
||||
</div>
|
||||
|
||||
<!-- Live preview of students in the selected class -->
|
||||
<div id="student-list-section" class="mt-2" style="display:none;">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered table-striped mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:50%;">Name</th>
|
||||
<th>Grade</th>
|
||||
<th>Gender</th>
|
||||
<th style="width:120px;">Qty</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="student-list-body">
|
||||
<!-- Filled dynamically -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- ===== Optional: Rendered Sheet (only if $stickers provided) ===== -->
|
||||
<?php if (!empty($stickers) && is_array($stickers)): ?>
|
||||
<style>
|
||||
/* Page + print */
|
||||
@page { size: A4; margin: 10mm; } /* Adjust as needed or map to page_size/orientation server-side */
|
||||
|
||||
:root{
|
||||
--gap: 4mm; /* space between stickers */
|
||||
--h: 34mm; /* default sticker height (fits 102x34 nicely) */
|
||||
}
|
||||
|
||||
/* PDF-safe baseline (2-up) */
|
||||
.sheet{ font-size:0; } /* collapse inline-block whitespace */
|
||||
.sticker{
|
||||
display:inline-block;
|
||||
vertical-align:top;
|
||||
width: calc(50% - var(--gap)/2); /* always two columns */
|
||||
height: var(--h);
|
||||
box-sizing:border-box;
|
||||
padding:4mm;
|
||||
margin:0 var(--gap) var(--gap) 0; /* right & bottom spacing */
|
||||
outline:0.2mm dashed #ccc; /* cut guide (optional) */
|
||||
page-break-inside:avoid;
|
||||
break-inside: avoid-page;
|
||||
}
|
||||
.sticker:nth-child(2n){ margin-right:0; }
|
||||
|
||||
.sticker h4{ margin:0 0 2mm; font-size:14pt; }
|
||||
.sticker p{ margin:0; font-size:11pt; }
|
||||
|
||||
/* Screen upgrade: grid when available (not used by PDF engines) */
|
||||
@media screen {
|
||||
.sheet{
|
||||
display:grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: var(--gap);
|
||||
font-size: initial;
|
||||
}
|
||||
.sticker{
|
||||
display:block;
|
||||
width:auto;
|
||||
margin:0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="sheet mt-4">
|
||||
<?php foreach ($stickers as $s):
|
||||
$title = $s['title'] ?? '';
|
||||
$line1 = $s['line1'] ?? '';
|
||||
$line2 = $s['line2'] ?? '';
|
||||
?>
|
||||
<div class="sticker">
|
||||
<?php if ($title !== ''): ?><h4><?= esc($title) ?></h4><?php endif; ?>
|
||||
<?php if ($line1 !== ''): ?><p><?= esc($line1) ?></p><?php endif; ?>
|
||||
<?php if ($line2 !== ''): ?><p><?= esc($line2) ?></p><?php endif; ?>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<!-- ===== End optional sheet ===== -->
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
<?= $this->section('scripts') ?>
|
||||
<script>
|
||||
(function() {
|
||||
const classSelect = document.getElementById('class_id');
|
||||
const studentSelect = document.getElementById('student_id');
|
||||
const listSection = document.getElementById('student-list-section');
|
||||
const listBody = document.getElementById('student-list-body');
|
||||
|
||||
const sizeSelect = document.getElementById('sticker_size');
|
||||
const customSizeRow = document.getElementById('customSizeRow');
|
||||
const inW = document.getElementById('sticker_width');
|
||||
const inH = document.getElementById('sticker_height');
|
||||
|
||||
const perPageInput = document.getElementById('stickers_per_page');
|
||||
const pageSize = document.getElementById('page_size');
|
||||
const orientation = document.getElementById('orientation');
|
||||
|
||||
const mL = document.getElementById('margin_left');
|
||||
const mT = document.getElementById('margin_top');
|
||||
const mR = document.getElementById('margin_right');
|
||||
const mB = document.getElementById('margin_bottom');
|
||||
const gX = document.getElementById('gap_x');
|
||||
const gY = document.getElementById('gap_y');
|
||||
|
||||
const settingsSummary = document.getElementById('settingsSummary');
|
||||
|
||||
// Keep student vs class mutually exclusive
|
||||
classSelect.addEventListener('change', () => {
|
||||
if (classSelect.value) {
|
||||
studentSelect.value = '';
|
||||
}
|
||||
});
|
||||
studentSelect.addEventListener('change', () => {
|
||||
if (studentSelect.value) {
|
||||
classSelect.value = '';
|
||||
listSection.style.display = 'none';
|
||||
listBody.innerHTML = '';
|
||||
updateSettingsSummary();
|
||||
}
|
||||
});
|
||||
|
||||
// Custom size toggling
|
||||
function toggleCustomSize() {
|
||||
const isCustom = sizeSelect.value === 'custom';
|
||||
customSizeRow.style.display = isCustom ? '' : 'none';
|
||||
if (!isCustom) {
|
||||
inW.value = '';
|
||||
inH.value = '';
|
||||
}
|
||||
}
|
||||
sizeSelect.addEventListener('change', () => { toggleCustomSize(); updateSettingsSummary(); });
|
||||
toggleCustomSize();
|
||||
|
||||
function updateSettingsSummary() {
|
||||
const sizeText = (sizeSelect.value === 'custom' && inW.value && inH.value)
|
||||
? `Custom ${inW.value}×${inH.value} mm`
|
||||
: sizeSelect.options[sizeSelect.selectedIndex].textContent;
|
||||
const per = `${perPageInput.value} per page`;
|
||||
const paper = `${pageSize.value} ${orientation.value === 'P' ? 'Portrait' : 'Landscape'}`;
|
||||
const margins = `margins L:${mL.value} T:${mT.value} R:${mR.value} B:${mB.value} mm`;
|
||||
const gaps = `gaps X:${gX.value} Y:${gY.value} mm`;
|
||||
settingsSummary.textContent = `Layout: ${sizeText}, ${per}, ${paper}, ${margins}, ${gaps}`;
|
||||
}
|
||||
[perPageInput, pageSize, orientation, mL, mT, mR, mB, gX, gY].forEach(el => {
|
||||
el.addEventListener('input', updateSettingsSummary);
|
||||
el.addEventListener('change', updateSettingsSummary);
|
||||
});
|
||||
updateSettingsSummary();
|
||||
|
||||
// Load students for class
|
||||
classSelect.addEventListener('change', function() {
|
||||
const classId = this.value;
|
||||
|
||||
listBody.innerHTML = '';
|
||||
listSection.style.display = 'none';
|
||||
|
||||
if (!classId) return;
|
||||
|
||||
fetch('<?= base_url('api/students/by_class') ?>/' + encodeURIComponent(classId))
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (!Array.isArray(data) || data.length === 0) {
|
||||
listSection.style.display = 'block';
|
||||
listBody.innerHTML = '<tr><td colspan="4" class="text-center text-muted">No students found for this class.</td></tr>';
|
||||
updateSettingsSummary();
|
||||
return;
|
||||
}
|
||||
|
||||
const fragRows = document.createDocumentFragment();
|
||||
data.forEach(stu => {
|
||||
const tr = document.createElement('tr');
|
||||
tr.innerHTML = `
|
||||
<td>${escapeHtml(stu.firstname)} ${escapeHtml(stu.lastname)}</td>
|
||||
<td>${stu.registration_grade ?? ''}</td>
|
||||
<td>${stu.gender ?? ''}</td>
|
||||
<td>
|
||||
<input type="number"
|
||||
class="form-control form-control-sm"
|
||||
name="copies[${stu.id}]"
|
||||
min="0" max="100" value="1"
|
||||
aria-label="Quantity for ${escapeHtml(stu.firstname)} ${escapeHtml(stu.lastname)}">
|
||||
</td>
|
||||
`;
|
||||
fragRows.appendChild(tr);
|
||||
});
|
||||
|
||||
listBody.appendChild(fragRows);
|
||||
listSection.style.display = 'block';
|
||||
updateSettingsSummary();
|
||||
})
|
||||
.catch(() => {
|
||||
listSection.style.display = 'block';
|
||||
listBody.innerHTML = '<tr><td colspan="4" class="text-center text-danger">Failed to load students. Please try again.</td></tr>';
|
||||
});
|
||||
});
|
||||
|
||||
// "Set all Qty" helper
|
||||
document.getElementById('btnFillAll').addEventListener('click', () => {
|
||||
const v = parseInt(document.getElementById('fillAllQty').value || '1', 10);
|
||||
listBody.querySelectorAll('input[name^="copies["]').forEach(inp => {
|
||||
inp.value = Math.max(0, Math.min(100, v));
|
||||
});
|
||||
});
|
||||
|
||||
function escapeHtml(s) {
|
||||
return String(s)
|
||||
.replaceAll('&', '&')
|
||||
.replaceAll('<', '<')
|
||||
.replaceAll('>', '>')
|
||||
.replaceAll('"', '"')
|
||||
.replaceAll("'", ''');
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
|
||||
<!-- Auto-count scripting -->
|
||||
<script>
|
||||
(() => {
|
||||
const btnAll = document.getElementById('btnAutoCountPrintAll');
|
||||
const btnClass = document.getElementById('btnAutoCountClass');
|
||||
const form = document.getElementById('autoPrintForm');
|
||||
|
||||
// Hidden fields in auto form
|
||||
const apSize = document.getElementById('ap_size');
|
||||
const apW = document.getElementById('ap_w');
|
||||
const apH = document.getElementById('ap_h');
|
||||
const apPPG = document.getElementById('ap_ppg');
|
||||
const apAll = document.getElementById('ap_print_all');
|
||||
const apClass = document.getElementById('ap_class_id');
|
||||
|
||||
const apPage = document.getElementById('ap_page_size');
|
||||
const apOrient = document.getElementById('ap_orientation');
|
||||
const apML = document.getElementById('ap_ml');
|
||||
const apMT = document.getElementById('ap_mt');
|
||||
const apMR = document.getElementById('ap_mr');
|
||||
const apMB = document.getElementById('ap_mb');
|
||||
const apGX = document.getElementById('ap_gx');
|
||||
const apGY = document.getElementById('ap_gy');
|
||||
|
||||
// Visible inputs to mirror
|
||||
const inSize = document.getElementById('sticker_size');
|
||||
const inW = document.getElementById('sticker_width');
|
||||
const inH = document.getElementById('sticker_height');
|
||||
const inPPG = document.getElementById('stickers_per_page');
|
||||
const selClass = document.getElementById('class_id');
|
||||
|
||||
const inPage = document.getElementById('page_size');
|
||||
const inOrient = document.getElementById('orientation');
|
||||
const inML = document.getElementById('margin_left');
|
||||
const inMT = document.getElementById('margin_top');
|
||||
const inMR = document.getElementById('margin_right');
|
||||
const inMB = document.getElementById('margin_bottom');
|
||||
const inGX = document.getElementById('gap_x');
|
||||
const inGY = document.getElementById('gap_y');
|
||||
|
||||
const totalsModal = new bootstrap.Modal(document.getElementById('previewTotalsModal'));
|
||||
const totalsBody = document.getElementById('previewTotalsBody');
|
||||
const btnProceed = document.getElementById('btnProceedPrint');
|
||||
|
||||
async function fetchCounts(classId = null) {
|
||||
const url = new URL('<?= base_url('stickers/preview') ?>', window.location.origin);
|
||||
if (classId) url.searchParams.set('class_id', classId);
|
||||
|
||||
const res = await fetch(url.toString(), {
|
||||
headers: { 'X-Requested-With': 'XMLHttpRequest' }
|
||||
});
|
||||
if (!res.ok) throw new Error('Preview failed: ' + res.status);
|
||||
const json = await res.json();
|
||||
if (json.status !== 'ok') throw new Error('Preview failed: bad status');
|
||||
return json.data; // { students:[{student_id, primary_count}], totals:{stickers, students} }
|
||||
}
|
||||
|
||||
function carryLayoutToHidden() {
|
||||
// Size / custom size
|
||||
apSize.value = inSize ? inSize.value : '';
|
||||
apW.value = (inSize.value === 'custom' && inW.value) ? inW.value : '';
|
||||
apH.value = (inSize.value === 'custom' && inH.value) ? inH.value : '';
|
||||
|
||||
// Cap per page
|
||||
apPPG.value = inPPG ? (inPPG.value || '') : '';
|
||||
|
||||
// Paper & orientation
|
||||
apPage.value = inPage ? inPage.value : 'A4';
|
||||
apOrient.value = inOrient ? inOrient.value : 'P';
|
||||
|
||||
// Margins & gaps
|
||||
apML.value = inML ? inML.value : '6';
|
||||
apMT.value = inMT ? inMT.value : '6';
|
||||
apMR.value = inMR ? inMR.value : apML.value;
|
||||
apMB.value = inMB ? inMB.value : '10';
|
||||
apGX.value = inGX ? inGX.value : '0';
|
||||
apGY.value = inGY ? inGY.value : '0';
|
||||
}
|
||||
|
||||
function fillHiddenCopies(students) {
|
||||
// Clean previous copies
|
||||
[...form.querySelectorAll('input[name^="copies["]')].forEach(n => n.remove());
|
||||
|
||||
// Use primary_count only
|
||||
for (const s of students) {
|
||||
const copies = Number(s.primary_count) || 0;
|
||||
if (copies <= 0) continue;
|
||||
const inp = document.createElement('input');
|
||||
inp.type = 'hidden';
|
||||
inp.name = `copies[${s.student_id}]`;
|
||||
inp.value = String(copies);
|
||||
form.appendChild(inp);
|
||||
}
|
||||
}
|
||||
|
||||
function showTotals(data) {
|
||||
let totalStickers = data?.totals?.stickers;
|
||||
if (typeof totalStickers !== 'number') {
|
||||
totalStickers = (data?.students ?? []).reduce(
|
||||
(sum, s) => sum + (Number(s.primary_count) || 0), 0
|
||||
);
|
||||
}
|
||||
const ns = data?.totals?.students ?? (data?.students?.length ?? 0);
|
||||
|
||||
totalsBody.innerHTML =
|
||||
`<div><strong>Students:</strong> ${ns}</div>` +
|
||||
`<div><strong>Total stickers:</strong> ${totalStickers}</div>`;
|
||||
|
||||
totalsModal.show();
|
||||
}
|
||||
|
||||
// Print All
|
||||
btnAll?.addEventListener('click', async () => {
|
||||
btnAll.disabled = true;
|
||||
try {
|
||||
const data = await fetchCounts(null); // all students
|
||||
showTotals(data);
|
||||
fillHiddenCopies(data.students);
|
||||
carryLayoutToHidden();
|
||||
apClass.value = '';
|
||||
apAll.value = '1';
|
||||
} catch (e) {
|
||||
alert(e.message || e);
|
||||
} finally {
|
||||
btnAll.disabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
// Print Selected Class
|
||||
btnClass?.addEventListener('click', async () => {
|
||||
const classId = selClass && selClass.value ? Number(selClass.value) : 0;
|
||||
if (!classId) {
|
||||
alert('Please select a class first.');
|
||||
return;
|
||||
}
|
||||
|
||||
btnClass.disabled = true;
|
||||
try {
|
||||
const data = await fetchCounts(classId);
|
||||
showTotals(data);
|
||||
fillHiddenCopies(data.students);
|
||||
carryLayoutToHidden();
|
||||
apClass.value = String(classId);
|
||||
apAll.value = '0';
|
||||
} catch (e) {
|
||||
alert(e.message || e);
|
||||
} finally {
|
||||
btnClass.disabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
// Proceed to print (submits hidden form; opens in new tab)
|
||||
btnProceed?.addEventListener('click', () => {
|
||||
totalsModal.hide();
|
||||
form.submit();
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
<?= $this->endSection() ?>
|
||||
Reference in New Issue
Block a user