/** * Public invoice / quote page. * * Everything the action bar does: print, PDF, send by email, the payment * panel, accepting or declining a quote, and the administrator's per-document * CSS editor. Configuration arrives in `easyInvoiceDocument` (localised by * DocumentPage::enqueue). */ (function ($) { 'use strict'; var cfg = window.easyInvoiceDocument || {}; var i18n = cfg.i18n || {}; var isQuote = cfg.type === 'quote'; /* ------------------------------------------------------------------ */ /* Modal */ /* ------------------------------------------------------------------ */ function closeModal(overlay) { if (!overlay) { return; } overlay.classList.remove('show'); var modal = overlay.querySelector('.ei-modal'); if (modal) { modal.classList.remove('show'); } setTimeout(function () { if (overlay.parentNode) { overlay.parentNode.removeChild(overlay); } }, 200); } function closeAnyModal() { var existing = document.querySelector('.ei-modal-overlay'); if (existing) { closeModal(existing); } } function iconFor(kind) { return { info: '?', danger: '!', success: '✓', error: '✕' }[kind] || '?'; } /** * Build a modal. * * @param {Object} o title, message, kind (info|danger|success|error), * confirmText, cancelText, reasonLabel (adds a textarea), onConfirm(reason, buttons) */ function openModal(o) { closeAnyModal(); var overlay = document.createElement('div'); overlay.className = 'ei-modal-overlay'; overlay.setAttribute('role', 'dialog'); overlay.setAttribute('aria-modal', 'true'); var reasonField = o.reasonLabel ? '' + '' : ''; var buttons = o.confirmText ? '' + '' : ''; overlay.innerHTML = '
' + '
' + '
' + iconFor(o.kind) + '
' + (o.title ? '

' + escapeHtml(o.title) + '

' : '') + '
' + '
' + (o.message ? '

' + escapeHtml(o.message) + '

' : '') + reasonField + '
' + '
' + buttons + '
' + '
'; document.body.appendChild(overlay); requestAnimationFrame(function () { overlay.classList.add('show'); overlay.querySelector('.ei-modal').classList.add('show'); }); var confirmBtn = overlay.querySelector('[data-modal="confirm"]'); var cancelBtn = overlay.querySelector('[data-modal="cancel"]'); function onKey(e) { if (e.key === 'Escape') { finish(); } } function finish() { document.removeEventListener('keydown', onKey); closeModal(overlay); if (typeof o.onClose === 'function') { o.onClose(); } } cancelBtn.addEventListener('click', finish); overlay.addEventListener('click', function (e) { if (e.target === overlay) { finish(); } }); document.addEventListener('keydown', onKey); if (confirmBtn) { confirmBtn.addEventListener('click', function () { var reason = ''; var textarea = overlay.querySelector('#ei-modal-reason'); if (textarea) { reason = textarea.value.trim(); if (o.reasonRequired && !reason) { textarea.classList.add('error'); textarea.focus(); return; } } o.onConfirm(reason, { busy: function (label) { confirmBtn.disabled = true; cancelBtn.disabled = true; confirmBtn.innerHTML = ' ' + escapeHtml(label || ''); }, close: function () { document.removeEventListener('keydown', onKey); closeModal(overlay); } }); }); (overlay.querySelector('#ei-modal-reason') || confirmBtn).focus(); } else { cancelBtn.focus(); } return overlay; } function showResult(kind, message, onClose) { openModal({ kind: kind, message: message, onClose: onClose }); } function escapeHtml(s) { return String(s === undefined || s === null ? '' : s) .replace(/&/g, '&').replace(//g, '>') .replace(/"/g, '"').replace(/'/g, '''); } function toast(kind, message) { if (window.EasyInvoiceToast && typeof window.EasyInvoiceToast[kind] === 'function') { window.EasyInvoiceToast[kind](message); } else { showResult(kind === 'success' ? 'success' : 'error', message); } } /* ------------------------------------------------------------------ */ /* AJAX */ /* ------------------------------------------------------------------ */ function post(data) { data = $.extend({ access_token: cfg.accessToken || '' }, data); return $.ajax({ url: cfg.ajaxUrl, type: 'POST', dataType: 'json', data: data }); } function errorMessage(response, fallback) { if (response && response.data) { if (typeof response.data === 'string') { return response.data; } if (response.data.message) { return response.data.message; } } return fallback; } /* ------------------------------------------------------------------ */ /* Actions */ /* ------------------------------------------------------------------ */ function printDocument() { window.print(); } /** * In-browser PDF: html2canvas + jsPDF over the page exactly as it is drawn * — the design, a custom template, the watermark layer. The default for * the Download button; also the fallback when the server cannot render. */ function browserPdf() { if (typeof window.DocumentPdfGenerator === 'undefined') { toast('error', i18n.pdfUnavailable || 'PDF generator not available.'); return; } try { new window.DocumentPdfGenerator(cfg.type).generatePDF(); } catch (e) { toast('error', i18n.pdfUnavailable || 'PDF generator not available.'); } } function sendByEmail() { openModal({ kind: 'info', title: i18n.sendTitle, message: i18n.sendMessage, confirmText: i18n.send, onConfirm: function (_reason, modal) { modal.busy(i18n.sending); var data = { action: isQuote ? 'easy_invoice_send_quote_email' : 'easy_invoice_send_invoice_email', nonce: (cfg.nonces || {}).sendEmail }; data[isQuote ? 'quote_id' : 'invoice_id'] = cfg.id; post(data).done(function (r) { modal.close(); if (r && r.success) { showResult('success', errorMessage(r, i18n.sent)); } else { showResult('error', errorMessage(r, i18n.sendFailed)); } }).fail(function () { modal.close(); showResult('error', i18n.networkError); }); } }); } /* Payment panel (invoices) */ function paymentPanel() { return document.getElementById('payment-panel'); } function payToggle() { return document.getElementById('toggle-payment-panel'); } function openPayment() { var panel = paymentPanel(), btn = payToggle(); if (!panel) { return; } panel.hidden = false; requestAnimationFrame(function () { panel.classList.add('is-open'); }); if (btn) { btn.classList.add('is-open'); btn.textContent = i18n.hidePayment || 'Hide payment'; } } function closePayment() { var panel = paymentPanel(), btn = payToggle(); if (!panel) { return; } panel.classList.remove('is-open'); setTimeout(function () { panel.hidden = true; }, 300); if (btn) { btn.classList.remove('is-open'); btn.textContent = btn.getAttribute('data-label') || 'Pay Now'; } } function togglePayment() { var panel = paymentPanel(); if (!panel) { return; } if (panel.hidden || !panel.classList.contains('is-open')) { openPayment(); } else { closePayment(); } } /* Signature pad (only when cfg.signature is set by an addon) */ function signaturePad(overlay) { var sig = cfg.signature || null; if (!sig) { return null; } var body = overlay.querySelector('.ei-modal-body'); var wrap = document.createElement('div'); wrap.className = 'ei-signature'; wrap.innerHTML = '' + '' + '' + '' + '
' + '' + escapeHtml(sig.hint || '') + '
'; body.appendChild(wrap); var canvas = wrap.querySelector('canvas'); var ctx = canvas.getContext('2d'); var drawing = false, drawn = false, last = null; ctx.lineWidth = 2; ctx.lineCap = 'round'; ctx.lineJoin = 'round'; ctx.strokeStyle = '#111'; function pos(e) { var r = canvas.getBoundingClientRect(); var p = e.touches ? e.touches[0] : e; return { x: (p.clientX - r.left) * (canvas.width / r.width), y: (p.clientY - r.top) * (canvas.height / r.height) }; } function start(e) { drawing = true; last = pos(e); e.preventDefault(); } function move(e) { if (!drawing) { return; } var p = pos(e); ctx.beginPath(); ctx.moveTo(last.x, last.y); ctx.lineTo(p.x, p.y); ctx.stroke(); last = p; drawn = true; e.preventDefault(); } function end() { drawing = false; } canvas.addEventListener('mousedown', start); canvas.addEventListener('mousemove', move); window.addEventListener('mouseup', end); canvas.addEventListener('touchstart', start, { passive: false }); canvas.addEventListener('touchmove', move, { passive: false }); canvas.addEventListener('touchend', end); wrap.querySelector('[data-sig=clear]').addEventListener('click', function () { ctx.clearRect(0, 0, canvas.width, canvas.height); drawn = false; }); return { required: !!sig.required, value: function () { return drawn ? canvas.toDataURL('image/png') : ''; }, name: function () { return wrap.querySelector('#ei-signer-name').value.trim(); }, invalid: function () { var bad = (sig.required && !drawn) || (sig.required && !this.name()); canvas.classList.toggle('error', sig.required && !drawn); wrap.querySelector('#ei-signer-name').classList.toggle('error', sig.required && !this.name()); return bad; } }; } /* Quote accept / decline */ function acceptQuote(button) { var description = button.getAttribute('data-description') || ''; var pad = null; var overlay = openModal({ kind: 'info', title: i18n.acceptTitle, message: description, confirmText: i18n.accept, onConfirm: function (_reason, modal) { if (pad && pad.invalid()) { return; } modal.busy(i18n.accepting); var data = { action: 'easy_invoice_accept_quote', quote_id: cfg.id, nonce: (cfg.nonces || {}).quoteAction }; if (pad) { data.signature = pad.value(); data.signer_name = pad.name(); } post(data).done(function (r) { modal.close(); if (!(r && r.success)) { showResult('error', errorMessage(r, i18n.actionFailed)); return; } var url = r.data && (r.data.secure_url || r.data.invoice_url); if (r.data && r.data.invoice_id && url) { showResult('success', errorMessage(r, i18n.acceptedRedirect), function () { window.location.href = url; }); setTimeout(function () { window.location.href = url; }, 1500); } else { showResult('success', errorMessage(r, i18n.accepted), function () { window.location.reload(); }); setTimeout(function () { window.location.reload(); }, 1500); } }).fail(function () { modal.close(); showResult('error', i18n.networkError); }); } }); pad = signaturePad(overlay); } function declineQuote(button) { var message = button.getAttribute('data-message') || i18n.declineMessage || ''; var reasonRequired = button.getAttribute('data-reason-required') === '1'; openModal({ kind: 'danger', title: i18n.declineTitle, message: message, confirmText: i18n.decline, reasonLabel: i18n.declineReason, reasonRequired: reasonRequired, onConfirm: function (reason, modal) { modal.busy(i18n.declining); var data = { action: 'easy_invoice_decline_quote', quote_id: cfg.id, nonce: (cfg.nonces || {}).quoteAction }; if (reason) { data.decline_reason = reason; } post(data).done(function (r) { modal.close(); if (r && r.success) { showResult('success', errorMessage(r, i18n.declined), function () { window.location.reload(); }); setTimeout(function () { window.location.reload(); }, 1500); } else { showResult('error', errorMessage(r, i18n.actionFailed)); } }).fail(function () { modal.close(); showResult('error', i18n.networkError); }); } }); } /* Administrator: per-document CSS */ function initCssPanel() { var toggle = document.getElementById('additional-css-btn'); var panel = document.getElementById('css-panel'); var overlay = document.getElementById('css-overlay'); if (!toggle || !panel || !overlay) { return; } var textarea = document.getElementById('additional-css-textarea'); var status = document.getElementById('css-status'); var styleId = 'custom-' + cfg.type + '-css'; function applyCss(css) { var el = document.getElementById(styleId); if (!el) { el = document.createElement('style'); el.id = styleId; document.head.appendChild(el); } el.textContent = css; } function say(kind, message) { status.hidden = false; status.className = 'ei-css-panel__status ' + (kind === 'success' ? 'is-success' : 'is-error'); status.textContent = message; setTimeout(function () { status.hidden = true; }, 3000); } function open() { panel.hidden = false; overlay.hidden = false; textarea.focus(); } function close() { panel.hidden = true; overlay.hidden = true; } function save(css) { return post({ action: 'save_additional_css', post_id: cfg.id, css: css, nonce: (cfg.nonces || {}).additionalCss }); } toggle.addEventListener('click', open); document.getElementById('close-css-panel').addEventListener('click', close); overlay.addEventListener('click', close); document.getElementById('preview-css-btn').addEventListener('click', function () { applyCss(textarea.value); }); document.getElementById('save-css-btn').addEventListener('click', function () { save(textarea.value).done(function (r) { if (r && r.success) { applyCss(textarea.value); toggle.classList.toggle('has-css', textarea.value.trim() !== ''); say('success', i18n.cssSaved); } else { say('error', errorMessage(r, i18n.cssFailed)); } }).fail(function () { say('error', i18n.networkError); }); }); document.getElementById('clear-css-btn').addEventListener('click', function () { textarea.value = ''; save('').done(function (r) { if (r && r.success) { applyCss(''); toggle.classList.remove('has-css'); say('success', i18n.cssCleared); } else { say('error', errorMessage(r, i18n.cssFailed)); } }).fail(function () { say('error', i18n.networkError); }); }); } /* ------------------------------------------------------------------ */ /* Wiring */ /* ------------------------------------------------------------------ */ document.addEventListener('click', function (e) { var el = e.target.closest('[data-ei-action]'); if (!el) { return; } switch (el.getAttribute('data-ei-action')) { case 'print': e.preventDefault(); printDocument(); break; case 'email': e.preventDefault(); sendByEmail(); break; case 'pay': e.preventDefault(); togglePayment(); break; case 'close-payment': e.preventDefault(); closePayment(); break; case 'accept': e.preventDefault(); acceptQuote(el); break; case 'decline': e.preventDefault(); declineQuote(el); break; case 'pdf': // Browser method: capture the page as drawn, right here, no // reload. Server method: the link itself serves the file. if (cfg.pdfMethod !== 'server') { e.preventDefault(); browserPdf(); } break; } }); document.addEventListener('DOMContentLoaded', function () { initCssPanel(); // The server serves ?auto_download_pdf=1 itself; reaching this page // with the flag means it could not, so fall back to the browser. if (cfg.autoDownload) { setTimeout(browserPdf, 800); } // A link that lands the client straight on the payment panel. if (cfg.openPayment) { setTimeout(openPayment, 300); } }); // Names the previous templates defined globally, kept for customised // design templates that call them. window.printInvoiceContent = printDocument; window.printQuoteContent = printDocument; window.downloadInvoicePdf = browserPdf; window.downloadQuotePdf = browserPdf; window.handleDownloadPDF = browserPdf; window.togglePaymentPanel = togglePayment; window.closePaymentPanel = closePayment; window.confirmSendEmail = sendByEmail; window.sendInvoiceEmail = sendByEmail; window.EasyInvoiceDocument = { modal: openModal, print: printDocument, sendByEmail: sendByEmail, openPayment: openPayment, closePayment: closePayment }; })(jQuery);