fix invoice and enrollment fees
This commit is contained in:
@@ -24,6 +24,25 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal fade" id="generateInvoiceConfirmModal" tabindex="-1" aria-labelledby="generateInvoiceConfirmModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="generateInvoiceConfirmModalLabel">Generate Invoice?</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p class="mb-2">Generate Invoice will recalculate this invoice using the current tuition settings and current student enrollment.</p>
|
||||
<p class="mb-0 text-muted small">This will update the saved invoice student list.</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-secondary" id="generateInvoiceNoButton" data-bs-dismiss="modal">No</button>
|
||||
<button type="button" class="btn btn-primary" id="generateInvoiceYesButton">Yes, Generate Invoice</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
<?= $this->section('scripts') ?>
|
||||
@@ -80,6 +99,7 @@
|
||||
|
||||
const fmtMoney = (v) => '$' + Number(v || 0).toFixed(2);
|
||||
const esc = (s) => $('<div>').text(String(s ?? '')).html();
|
||||
const escAttr = (s) => esc(s).replace(/`/g, '`');
|
||||
const pad2 = (num) => String(num).padStart(2, '0');
|
||||
const formatDateTime = (ts) => {
|
||||
const date = new Date(ts);
|
||||
@@ -130,7 +150,7 @@
|
||||
const ts = Date.parse(r.invoice_date || new Date().toISOString());
|
||||
const genBtn = isCarryForward
|
||||
? '<span class="text-muted small">Audit only</span>'
|
||||
: `<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>`;
|
||||
: `<button type=\"button\" class=\"btn btn-primary btn-sm gen-invoice\" data-parent-id=\"${escAttr(r.parent_id)}\" data-parent-name=\"${escAttr(r.parent_name || '')}\">Generate Invoice</button>`;
|
||||
const invoiceLabel = isCarryForward
|
||||
? `<div><span class="badge bg-secondary me-1">Carry-over</span>${esc(r.invoice_description || 'Carry over balance')}</div>`
|
||||
+ (r.invoice_number ? `<div class="text-muted small">${esc(r.invoice_number)}</div>` : '')
|
||||
@@ -169,6 +189,47 @@
|
||||
return json || { ok: true };
|
||||
}
|
||||
|
||||
function confirmGenerateInvoice(parentName) {
|
||||
return new Promise((resolve) => {
|
||||
const modalEl = document.getElementById('generateInvoiceConfirmModal');
|
||||
const yesButton = document.getElementById('generateInvoiceYesButton');
|
||||
if (!modalEl || !yesButton || typeof bootstrap === 'undefined') {
|
||||
resolve(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const body = modalEl.querySelector('.modal-body');
|
||||
if (body) {
|
||||
const name = parentName ? `<strong>${esc(parentName)}</strong>` : 'this parent';
|
||||
body.innerHTML = '<p class="mb-2">Generate Invoice for ' + name + '?</p>'
|
||||
+ '<p class="mb-2">This will recalculate the invoice using the current tuition settings and current student enrollment.</p>'
|
||||
+ '<p class="mb-0 text-muted small">This will update the saved invoice student list.</p>';
|
||||
}
|
||||
|
||||
const modal = bootstrap.Modal.getOrCreateInstance(modalEl);
|
||||
let settled = false;
|
||||
const cleanup = () => {
|
||||
yesButton.removeEventListener('click', onYes);
|
||||
modalEl.removeEventListener('hidden.bs.modal', onHidden);
|
||||
};
|
||||
const finish = (value) => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
cleanup();
|
||||
resolve(value);
|
||||
};
|
||||
const onYes = () => {
|
||||
modal.hide();
|
||||
finish(true);
|
||||
};
|
||||
const onHidden = () => finish(false);
|
||||
|
||||
yesButton.addEventListener('click', onYes);
|
||||
modalEl.addEventListener('hidden.bs.modal', onHidden, { once: true });
|
||||
modal.show();
|
||||
});
|
||||
}
|
||||
|
||||
async function loadInvoices(year) {
|
||||
const url = year ? `${API_URL}?schoolYear=${encodeURIComponent(year)}` : API_URL;
|
||||
const res = await fetch(url, { credentials: 'same-origin' });
|
||||
@@ -188,27 +249,33 @@
|
||||
const selected = resp.schoolYear || (years[0] || '');
|
||||
$year.innerHTML = years.map(y => `<option value="${esc(y)}" ${y===selected?'selected':''}>${esc(y)}</option>`).join('');
|
||||
};
|
||||
// Global fallback handler for inline onclick
|
||||
window.__genInvoice = async function(btn){
|
||||
|
||||
const runGenerateInvoice = async function(btn) {
|
||||
const confirmed = await confirmGenerateInvoice(btn.getAttribute('data-parent-name') || '');
|
||||
if (!confirmed) return false;
|
||||
|
||||
try {
|
||||
btn.disabled = true;
|
||||
await generateInvoice(btn.getAttribute('data-parent-id'));
|
||||
const resp = await loadInvoices(selectedSchoolYear());
|
||||
syncSchoolYearSelect(resp);
|
||||
const resp = await loadInvoices(selectedSchoolYear());
|
||||
syncSchoolYearSelect(resp);
|
||||
|
||||
const data = (resp.invoices || []).map(renderInvoiceRow);
|
||||
const data = (resp.invoices || []).map(renderInvoiceRow);
|
||||
if ($.fn.DataTable.isDataTable($tbl)) {
|
||||
const dti = $tbl.DataTable();
|
||||
dti.clear();
|
||||
dti.rows.add(data).draw(false);
|
||||
}
|
||||
showToast('Invoice generated');
|
||||
} catch (err) {
|
||||
showToast(err?.message || 'Failed to generate invoice', false);
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
window.__genInvoice = runGenerateInvoice;
|
||||
|
||||
try {
|
||||
const resp = await loadInvoices();
|
||||
syncSchoolYearSelect(resp);
|
||||
@@ -233,47 +300,11 @@
|
||||
}
|
||||
|
||||
// Delegate generate invoice
|
||||
// Native delegation
|
||||
document.addEventListener('click', async (e) => {
|
||||
const btn = e.target.closest('.gen-invoice');
|
||||
if (!btn) return;
|
||||
btn.disabled = true;
|
||||
try {
|
||||
await generateInvoice(btn.getAttribute('data-parent-id'));
|
||||
// Refresh table minimally: reload data
|
||||
const resp = await loadInvoices(selectedSchoolYear());
|
||||
const data = (resp.invoices || []).map(renderInvoiceRow);
|
||||
if ($.fn.DataTable.isDataTable($tbl)) {
|
||||
const dti = $tbl.DataTable();
|
||||
dti.clear();
|
||||
dti.rows.add(data).draw(false);
|
||||
}
|
||||
showToast('Invoice generated');
|
||||
} catch (err) {
|
||||
showToast(err?.message || 'Failed to generate invoice', false);
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
// jQuery delegated fallback
|
||||
$(document).on('click', '.gen-invoice', async function (e) {
|
||||
try {
|
||||
this.disabled = true;
|
||||
await generateInvoice(this.getAttribute('data-parent-id'));
|
||||
const resp = await loadInvoices(selectedSchoolYear());
|
||||
const data = (resp.invoices || []).map(renderInvoiceRow);
|
||||
if ($.fn.DataTable.isDataTable($tbl)) {
|
||||
const dti = $tbl.DataTable();
|
||||
dti.clear();
|
||||
dti.rows.add(data).draw(false);
|
||||
}
|
||||
showToast('Invoice generated');
|
||||
} catch (err) {
|
||||
showToast(err?.message || 'Failed to generate invoice', false);
|
||||
} finally {
|
||||
this.disabled = false;
|
||||
}
|
||||
e.preventDefault();
|
||||
await runGenerateInvoice(btn);
|
||||
});
|
||||
|
||||
// Explicit delegated handler as a safety net (in addition to inline onclick)
|
||||
|
||||
Reference in New Issue
Block a user