/** * invoice-modal.js — Invoice create/edit modal * Uses shared item-editor for accordion items * * Features: * - Auto-sets tax-exempt based on customer's taxable flag * - Recurring invoice support (monthly/yearly) */ import { addItem, getItems, resetItemCounter } from '../utils/item-editor.js'; import { setDefaultDate, showSpinner, hideSpinner } from '../utils/helpers.js'; let currentInvoiceId = null; let qboLaborRate = null; export async function loadLaborRate() { try { const response = await fetch('/api/qbo/labor-rate'); const data = await response.json(); if (data.rate) { qboLaborRate = data.rate; console.log(`💰 Labor Rate geladen: $${qboLaborRate}`); } } catch (e) { console.log('Labor Rate konnte nicht geladen werden.'); } } export function getLaborRate() { return qboLaborRate; } /** * Auto-set tax exempt based on customer's taxable flag */ function applyCustomerTaxStatus(customerId) { const allCust = window.getCustomers ? window.getCustomers() : (window.customers || []); const customer = allCust.find(c => c.id === parseInt(customerId)); if (customer) { const cb = document.getElementById('invoice-tax-exempt'); if (cb) { cb.checked = (customer.taxable === false); updateInvoiceTotals(); } } } export async function openInvoiceModal(invoiceId = null) { currentInvoiceId = invoiceId; if (invoiceId) { await loadInvoiceForEdit(invoiceId); } else { prepareNewInvoice(); } document.getElementById('invoice-modal').classList.add('active'); } export function closeInvoiceModal() { document.getElementById('invoice-modal').classList.remove('active'); currentInvoiceId = null; } async function loadInvoiceForEdit(invoiceId) { document.getElementById('invoice-modal-title').textContent = 'Edit Invoice'; const response = await fetch(`/api/invoices/${invoiceId}`); const data = await response.json(); // Set customer in Alpine component const allCust = window.getCustomers ? window.getCustomers() : (window.customers || []); const customer = allCust.find(c => c.id === data.invoice.customer_id); if (customer) { const customerInput = document.querySelector('#invoice-modal input[placeholder="Search customer..."]'); if (customerInput) { customerInput.value = customer.name; customerInput.dispatchEvent(new Event('input')); const alpineData = Alpine.$data(customerInput.closest('[x-data]')); if (alpineData) { alpineData.search = customer.name; alpineData.selectedId = customer.id; alpineData.selectedName = customer.name; } } } document.getElementById('invoice-number').value = data.invoice.invoice_number || ''; document.getElementById('invoice-customer').value = data.invoice.customer_id; document.getElementById('invoice-date').value = data.invoice.invoice_date.split('T')[0]; document.getElementById('invoice-terms').value = data.invoice.terms; document.getElementById('invoice-authorization').value = data.invoice.auth_code || ''; document.getElementById('invoice-tax-exempt').checked = data.invoice.tax_exempt; document.getElementById('invoice-bill-to-name').value = data.invoice.bill_to_name || ''; const sendDateEl = document.getElementById('invoice-send-date'); if (sendDateEl) { sendDateEl.value = data.invoice.scheduled_send_date ? data.invoice.scheduled_send_date.split('T')[0] : ''; } // Recurring fields const recurringCb = document.getElementById('invoice-recurring'); const recurringInterval = document.getElementById('invoice-recurring-interval'); const recurringGroup = document.getElementById('invoice-recurring-group'); if (recurringCb) { recurringCb.checked = data.invoice.is_recurring || false; if (recurringInterval) recurringInterval.value = data.invoice.recurring_interval || 'monthly'; if (recurringGroup) recurringGroup.style.display = data.invoice.is_recurring ? 'block' : 'none'; } // Load items document.getElementById('invoice-items').innerHTML = ''; resetItemCounter(); data.items.forEach(item => { addItem('invoice-items', { item, type: 'invoice', laborRate: qboLaborRate, onUpdate: updateInvoiceTotals }); }); updateInvoiceTotals(); } function prepareNewInvoice() { document.getElementById('invoice-modal-title').textContent = 'New Invoice'; document.getElementById('invoice-form').reset(); document.getElementById('invoice-items').innerHTML = ''; document.getElementById('invoice-terms').value = 'Net 30'; document.getElementById('invoice-number').value = ''; document.getElementById('invoice-send-date').value = ''; // Reset recurring const recurringCb = document.getElementById('invoice-recurring'); const recurringGroup = document.getElementById('invoice-recurring-group'); if (recurringCb) recurringCb.checked = false; if (recurringGroup) recurringGroup.style.display = 'none'; resetItemCounter(); setDefaultDate(); addItem('invoice-items', { type: 'invoice', laborRate: qboLaborRate, onUpdate: updateInvoiceTotals }); } export function addInvoiceItem(item = null) { addItem('invoice-items', { item, type: 'invoice', laborRate: qboLaborRate, onUpdate: updateInvoiceTotals }); } export function updateInvoiceTotals() { const items = getItems('invoice-items'); const taxExempt = document.getElementById('invoice-tax-exempt').checked; let subtotal = 0; items.forEach(item => { const amount = parseFloat(item.amount.replace(/[$,]/g, '')) || 0; subtotal += amount; }); const taxAmount = taxExempt ? 0 : (subtotal * 8.25 / 100); const total = subtotal + taxAmount; document.getElementById('invoice-subtotal').textContent = `$${subtotal.toFixed(2)}`; document.getElementById('invoice-tax').textContent = taxExempt ? '$0.00' : `$${taxAmount.toFixed(2)}`; document.getElementById('invoice-total').textContent = `$${total.toFixed(2)}`; document.getElementById('invoice-tax-row').style.display = taxExempt ? 'none' : 'block'; } export async function handleInvoiceSubmit(e) { e.preventDefault(); const isRecurring = document.getElementById('invoice-recurring')?.checked || false; const recurringInterval = isRecurring ? (document.getElementById('invoice-recurring-interval')?.value || 'monthly') : null; const data = { invoice_number: document.getElementById('invoice-number').value || null, customer_id: document.getElementById('invoice-customer').value, invoice_date: document.getElementById('invoice-date').value, terms: document.getElementById('invoice-terms').value, auth_code: document.getElementById('invoice-authorization').value, tax_exempt: document.getElementById('invoice-tax-exempt').checked, scheduled_send_date: document.getElementById('invoice-send-date')?.value || null, bill_to_name: document.getElementById('invoice-bill-to-name')?.value || null, is_recurring: isRecurring, recurring_interval: recurringInterval, items: getItems('invoice-items') }; if (!data.customer_id) { alert('Please select a customer.'); return; } if (!data.items || data.items.length === 0) { alert('Please add at least one item.'); return; } const invoiceId = currentInvoiceId; const url = invoiceId ? `/api/invoices/${invoiceId}` : '/api/invoices'; const method = invoiceId ? 'PUT' : 'POST'; showSpinner(invoiceId ? 'Saving invoice & syncing QBO...' : 'Creating invoice & exporting to QBO...'); try { const response = await fetch(url, { method, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); const result = await response.json(); if (response.ok) { closeInvoiceModal(); if (result.qbo_doc_number) console.log(`✅ Invoice saved & exported to QBO: #${result.qbo_doc_number}`); else if (result.qbo_synced) console.log('✅ Invoice saved & synced to QBO'); else console.log('✅ Invoice saved locally (QBO sync pending)'); if (window.invoiceView) window.invoiceView.loadInvoices(); } else { alert(`Error: ${result.error}`); } } catch (error) { console.error('Error:', error); alert('Error saving invoice'); } finally { hideSpinner(); } } export function initInvoiceModal() { const form = document.getElementById('invoice-form'); if (form) form.addEventListener('submit', handleInvoiceSubmit); const taxExempt = document.getElementById('invoice-tax-exempt'); if (taxExempt) taxExempt.addEventListener('change', updateInvoiceTotals); // Recurring toggle const recurringCb = document.getElementById('invoice-recurring'); const recurringGroup = document.getElementById('invoice-recurring-group'); if (recurringCb && recurringGroup) { recurringCb.addEventListener('change', () => { recurringGroup.style.display = recurringCb.checked ? 'block' : 'none'; }); } // Watch for customer selection → auto-set tax exempt (only for new invoices) const customerHidden = document.getElementById('invoice-customer'); if (customerHidden) { const observer = new MutationObserver(() => { // Only auto-apply when creating new (not editing existing) if (!currentInvoiceId && customerHidden.value) { applyCustomerTaxStatus(customerHidden.value); } }); observer.observe(customerHidden, { attributes: true, attributeFilter: ['value'] }); } } window.openInvoiceModal = openInvoiceModal; window.closeInvoiceModal = closeInvoiceModal; window.addInvoiceItem = addInvoiceItem;