Fix semester context, attendance rosters, and billing workflows
Deploy to Shared Hosting / Shared hosting deploy (push) Failing after 31s
Tests / PHPUnit (push) Failing after 55s

- load global semester helpers consistently and use date-based semester defaults
- fix grading and daily attendance duplicate student/section rows
- keep attendance violations scoped to the current semester by default
- update invoice, refund, discount, payment, and financial aid flows
- add configuration cleanup migrations for duplicate calendar/semester keys
- refresh parent registration/report-card and print request handling
- update related models, services, views, cron notes, and test coverage
This commit is contained in:
root
2026-08-16 17:41:11 -04:00
parent 36c7e3fc6d
commit 0ac3a8375e
99 changed files with 1598 additions and 814 deletions
@@ -104,6 +104,18 @@
if (!list || !list.length) return '<em>No students enrolled.</em>';
return list.map(k => `${esc(k.name)} (Grade: ${esc(k.grade)})`).join('<br>');
}
function renderRefundCell(r) {
const details = Array.isArray(r.refund_details) ? r.refund_details : [];
const lines = details.map(d => {
const ts = d.date ? Date.parse(d.date) : NaN;
const date = Number.isNaN(ts) ? '-' : formatDateTime(ts);
const method = d.method ? esc(d.method) : '-';
const check = d.check_number ? `, Check # ${esc(d.check_number)}` : '';
return `<div class="text-muted small">${date} &middot; ${method}${check}</div>`;
}).join('');
return `<div class="fw-semibold">${fmtMoney(r.refund_amount)}</div>${lines}`;
}
function renderParentCell(r) {
const pid = parseInt(r.parent_id || 0, 10);
const name = esc(r.parent_name || '');
@@ -113,6 +125,21 @@
}
return name;
}
function renderInvoiceRow(r) {
const ts = Date.parse(r.invoice_date || new Date().toISOString());
const genBtn = `<button type=\"button\" class=\"btn btn-primary btn-sm gen-invoice\" data-parent-id=\"${esc(r.parent_id)}\" onclick=\"return window.__genInvoice && window.__genInvoice(this)\">Generate Invoice</button>`;
const pdf = r.invoice_id ? `<a href=\"<?= base_url('invoice/pdf') ?>/${esc(r.invoice_id)}\" target=\"_blank\" class=\"btn btn-info btn-sm external-link\">View/Print PDF</a>` : '<span class=\"text-muted\">No invoice yet</span>';
return [
renderParentCell(r),
renderStudents(r.enrolledKids || []),
genBtn,
fmtMoney(r.invoice_amount),
renderRefundCell(r),
`<span data-order=\"${ts}\">${formatDateTime(ts)}</span>`,
pdf,
];
}
async function generateInvoice(parentId) {
const body = new URLSearchParams();
@@ -160,20 +187,7 @@
const resp = await loadInvoices(selectedSchoolYear());
syncSchoolYearSelect(resp);
const data = (resp.invoices || []).map(r => {
const ts = Date.parse(r.invoice_date || new Date().toISOString());
const genBtn = `<button type=\"button\" class=\"btn btn-primary btn-sm gen-invoice\" data-parent-id=\"${esc(r.parent_id)}\" onclick=\"return window.__genInvoice && window.__genInvoice(this)\">Generate Invoice</button>`;
const pdf = r.invoice_id ? `<a href=\"<?= base_url('invoice/pdf') ?>/${esc(r.invoice_id)}\" target=\"_blank\" class=\"btn btn-info btn-sm external-link\">View/Print PDF</a>` : '<span class=\"text-muted\">No invoice yet</span>';
return [
renderParentCell(r),
renderStudents(r.enrolledKids || []),
genBtn,
fmtMoney(r.invoice_amount),
fmtMoney(r.refund_amount),
`<span data-order=\"${ts}\">${formatDateTime(ts)}</span>`,
pdf,
];
});
const data = (resp.invoices || []).map(renderInvoiceRow);
if ($.fn.DataTable.isDataTable($tbl)) {
const dti = $tbl.DataTable();
dti.clear();
@@ -190,20 +204,7 @@
const resp = await loadInvoices();
syncSchoolYearSelect(resp);
const data = (resp.invoices || []).map(r => {
const ts = Date.parse(r.invoice_date || new Date().toISOString());
const genBtn = `<button type=\"button\" class=\"btn btn-primary btn-sm gen-invoice\" data-parent-id=\"${esc(r.parent_id)}\" onclick=\"return window.__genInvoice && window.__genInvoice(this)\">Generate Invoice</button>`;
const pdf = r.invoice_id ? `<a href=\"<?= base_url('invoice/pdf') ?>/${esc(r.invoice_id)}\" target=\"_blank\" class=\"btn btn-info btn-sm external-link\">View/Print PDF</a>` : '<span class=\"text-muted\">No invoice yet</span>';
return [
renderParentCell(r),
renderStudents(r.enrolledKids || []),
genBtn,
fmtMoney(r.invoice_amount),
fmtMoney(r.refund_amount),
`<span data-order=\"${ts}\">${formatDateTime(ts)}</span>`,
pdf,
];
});
const data = (resp.invoices || []).map(renderInvoiceRow);
dt = $tbl.DataTable({
data,
@@ -232,20 +233,7 @@
await generateInvoice(btn.getAttribute('data-parent-id'));
// Refresh table minimally: reload data
const resp = await loadInvoices(selectedSchoolYear());
const data = (resp.invoices || []).map(r => {
const ts = Date.parse(r.invoice_date || new Date().toISOString());
const genBtn = `<button type=\"button\" class=\"btn btn-primary btn-sm gen-invoice\" data-parent-id=\"${esc(r.parent_id)}\" onclick=\"return window.__genInvoice && window.__genInvoice(this)\">Generate Invoice</button>`;
const pdf = r.invoice_id ? `<a href=\"<?= base_url('invoice/pdf') ?>/${esc(r.invoice_id)}\" target=\"_blank\" class=\"btn btn-info btn-sm external-link\">View/Print PDF</a>` : '<span class=\"text-muted\">No invoice yet</span>';
return [
renderParentCell(r),
renderStudents(r.enrolledKids || []),
genBtn,
fmtMoney(r.invoice_amount),
fmtMoney(r.refund_amount),
`<span data-order=\"${ts}\">${formatDateTime(ts)}</span>`,
pdf,
];
});
const data = (resp.invoices || []).map(renderInvoiceRow);
if ($.fn.DataTable.isDataTable($tbl)) {
const dti = $tbl.DataTable();
dti.clear();
@@ -265,20 +253,7 @@
this.disabled = true;
await generateInvoice(this.getAttribute('data-parent-id'));
const resp = await loadInvoices(selectedSchoolYear());
const data = (resp.invoices || []).map(r => {
const ts = Date.parse(r.invoice_date || new Date().toISOString());
const genBtn = `<button type=\"button\" class=\"btn btn-primary btn-sm gen-invoice\" data-parent-id=\"${esc(r.parent_id)}\" onclick=\"return window.__genInvoice && window.__genInvoice(this)\">Generate Invoice</button>`;
const pdf = r.invoice_id ? `<a href=\"<?= base_url('invoice/pdf') ?>/${esc(r.invoice_id)}\" target=\"_blank\" class=\"btn btn-info btn-sm external-link\">View/Print PDF</a>` : '<span class=\"text-muted\">No invoice yet</span>';
return [
esc(r.parent_name || ''),
renderStudents(r.enrolledKids || []),
genBtn,
fmtMoney(r.invoice_amount),
fmtMoney(r.refund_amount),
`<span data-order=\"${ts}\">${formatDateTime(ts)}</span>`,
pdf,
];
});
const data = (resp.invoices || []).map(renderInvoiceRow);
if ($.fn.DataTable.isDataTable($tbl)) {
const dti = $tbl.DataTable();
dti.clear();
@@ -307,20 +282,7 @@
$year.addEventListener('change', async () => {
try {
const resp = await loadInvoices($year.value);
const data = (resp.invoices || []).map(r => {
const ts = Date.parse(r.invoice_date || new Date().toISOString());
const genBtn = `<button type=\"button\" class=\"btn btn-primary btn-sm gen-invoice\" data-parent-id=\"${esc(r.parent_id)}\" onclick=\"return window.__genInvoice && window.__genInvoice(this)\">Generate Invoice</button>`;
const pdf = r.invoice_id ? `<a href=\"<?= base_url('invoice/pdf') ?>/${esc(r.invoice_id)}\" target=\"_blank\" class=\"btn btn-info btn-sm external-link\">View/Print PDF</a>` : '<span class=\"text-muted\">No invoice yet</span>';
return [
esc(r.parent_name || ''),
renderStudents(r.enrolledKids || []),
genBtn,
fmtMoney(r.invoice_amount),
fmtMoney(r.refund_amount),
`<span data-order=\"${ts}\">${formatDateTime(ts)}</span>`,
pdf,
];
});
const data = (resp.invoices || []).map(renderInvoiceRow);
if ($.fn.DataTable.isDataTable($tbl)) {
const dti = $tbl.DataTable();
dti.clear();