class BillingWidget { constructor() { this.initialize(); } initialize() { document.addEventListener('click', this.onClick.bind(this)) document.addEventListener('keyup', this.onChange.bind(this)) document.addEventListener('focus', this.onFocus.bind(this)) } /** * @param {KeyboardEvent} event */ onFocus(event) { let target = event.target let element; if ((element = target.closest('[data-type="price"]')) || (element = target.closest('[data-type="vat"]'))) { if (element.value === '0.00') { element.value = ''; } } } /** * @param {KeyboardEvent} event */ onChange(event) { let target = event.target let element; if (element = target.closest('[data-refresh-billing]')) { this.refreshBilling() } } /** * @param {PointerEvent} event */ onClick(event) { let target = event.target let element; if (element = target.closest('[data-button-action="pdf"]')) { if ('1' === element.dataset.printed) { window.open(element.dataset.url); } else { this.billingCheckPDF(element); } } if (element = target.closest('[data-button-action="save"]')) { let form = element.closest('form'); app.formSubmitSpinner(true) app.setRefreshingState(true); fetch(form.getAttribute('action'), Commons.getRawPostOptions(form)) .then(response => response.json()) .then(json => Commons.handleAjaxResponse(json)) .finally(() => { app.formSubmitSpinner(false) app.setRefreshingState(false); }) } if (element = target.closest('[data-load-billing-modal]')) { app.toggleOverlay(); fetch(element.dataset.url) .then(response => response.text()) .then(text => { document.getElementById('billing-modal-container').innerHTML = text; Commons.showModal('invoice-modal') this.refreshBilling(); CellManagement.updateAllDates(); }) .finally(() => { app.toggleOverlay(); }) } } refreshBilling() { let modal = document.getElementById('invoice-modal'); let billingLines = []; let i = 0; let total = 0; let total_incl = 0; modal.querySelectorAll('tr[data-type="billing-line"]').forEach( el => { let price = this.getFloatFrom(el.querySelector("[data-type=price]")); let vat = this.getFloatFrom(el.querySelector("[data-type=vat]")); let incl_vat = price + (price * vat / 100.0); billingLines[i] = { price: price, vat: vat, incl_vat_price: incl_vat }; total += price; total_incl += incl_vat; i++; } ) let consultation_fees = this.getFloatFrom(modal.querySelector('[name="consultation_fees"]')); let consultation_fees_vat = this.getFloatFrom(modal.querySelector('[name="consultation_fees_vat_rate"]')); total += consultation_fees; total_incl += consultation_fees + (consultation_fees * consultation_fees_vat / 100.0); let discount = this.getFloatFrom(modal.querySelector('[name="discount_rate"]')); let total_due = total * (1 - discount / 100.0); let total_due_incl_tax = total_incl * (1 - discount / 100.0); let total_discount = total_incl * (discount / 100.0); if (discount > 0.01) { modal.querySelector('[data-type="total"]')?.classList?.remove('d-none'); modal.querySelector('[data-type="discount"]')?.classList?.remove('d-none'); modal.querySelector('[data-type="discount"] span[data-percent-dest]').innerHTML = modal.querySelector('[data-type="discount"] span[data-percent-src]').innerHTML.replace(':percent', discount); modal.querySelector('[data-type="total"] span[data-value]').innerHTML = total_incl.toFixed(2); modal.querySelector('[data-type="discount"] span[data-value]').innerHTML = total_discount.toFixed(2); modal.querySelector('[data-type="taxes"] span[data-value]').innerHTML = (total_due_incl_tax - total_due).toFixed(2); modal.querySelector('[data-type="total_due"] span[data-value]').innerHTML = total_due.toFixed(2); modal.querySelector('[data-type="total_due_incl_tax"] span[data-value]').innerHTML = total_due_incl_tax.toFixed(2); } else { modal.querySelector('[data-type="total"]')?.classList?.add('d-none'); modal.querySelector('[data-type="discount"]')?.classList?.add('d-none'); modal.querySelector('[data-type="taxes"] span[data-value]').innerHTML = (total_due_incl_tax - total_due).toFixed(2); modal.querySelector('[data-type="total_due"] span[data-value]').innerHTML = total.toFixed(2); modal.querySelector('[data-type="total_due_incl_tax"] span[data-value]').innerHTML = total_incl.toFixed(2); } modal.querySelectorAll('[data-type-currency]').forEach(el => { el.innerHTML = modal.querySelector("input[name=currencySymbol]").value }) } /** * @param {HTMLInputElement} input * @return {number} */ getFloatFrom(input) { return this.fromNaNToZero(parseFloat(input.value)); } /** * @param {number} number * @return {number} */ fromNaNToZero(number) { return isNaN(number) ? 0 : number; } billingCheckPDF(el) { el.disabled = true; Commons.setSpinner(el); fetch(el.dataset.checkUrl) .then(response => response.json()) .then(data => { if (data.status) { PageNotifier.confirmDangerousAction( __('page.billing.validate.title'), __('page.billing.validate.disclaimer'), __('page.billing.validate.ok'), __('page.billing.validate.edit') ).then((result) => { if (result) { window.open(el.dataset.url); } }); } else { let profileDiv = document.querySelector("#billing-missing-fields .missing-info .profile"); profileDiv?.classList.add('d-none'); let patientDiv = document.querySelector("#billing-missing-fields .missing-info .patient"); patientDiv?.classList.add('d-none'); profileDiv.querySelector('ol').innerHTML = ''; patientDiv.querySelector('ol').innerHTML = ''; profileDiv.querySelector('a').href = data.profileLink; patientDiv.querySelector('a').href = data.patientLink; Object.values(data.profileFields).forEach(field => { let li = document.createElement('li') li.innerHTML = field profileDiv.classList.remove('d-none') profileDiv.querySelector('ol').append(li) }) Object.values(data.patientFields).forEach(field => { let li = document.createElement('li') li.innerHTML = field patientDiv.classList.remove('d-none') patientDiv.querySelector('ol').append(li) }) Commons.showModal('billing-missing-fields'); } }) .finally(() => { el.disabled = false; Commons.unsetSpinner(el); }) } }