233 lines
9.8 KiB
JavaScript
233 lines
9.8 KiB
JavaScript
/**
|
|
* 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; |