recurring, tax exempt, badge

This commit is contained in:
2026-03-04 18:21:40 -06:00
parent e333628f1c
commit e9d88b1400
7 changed files with 323 additions and 70 deletions

View File

@@ -1,6 +1,10 @@
/**
* 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';
@@ -8,9 +12,6 @@ import { setDefaultDate, showSpinner, hideSpinner } from '../utils/helpers.js';
let currentInvoiceId = null;
let qboLaborRate = null;
/**
* Load labor rate from QBO (called once at startup)
*/
export async function loadLaborRate() {
try {
const response = await fetch('/api/qbo/labor-rate');
@@ -20,23 +21,34 @@ export async function loadLaborRate() {
console.log(`💰 Labor Rate geladen: $${qboLaborRate}`);
}
} catch (e) {
console.log('Labor Rate konnte nicht geladen werden, verwende keinen Default.');
console.log('Labor Rate konnte nicht geladen werden.');
}
}
export function getLaborRate() {
return qboLaborRate;
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');
}
@@ -47,7 +59,6 @@ export function closeInvoiceModal() {
async function loadInvoiceForEdit(invoiceId) {
document.getElementById('invoice-modal-title').textContent = 'Edit Invoice';
const response = await fetch(`/api/invoices/${invoiceId}`);
const data = await response.json();
@@ -79,22 +90,25 @@ async function loadInvoiceForEdit(invoiceId) {
const sendDateEl = document.getElementById('invoice-send-date');
if (sendDateEl) {
sendDateEl.value = data.invoice.scheduled_send_date
? data.invoice.scheduled_send_date.split('T')[0]
: '';
? data.invoice.scheduled_send_date.split('T')[0] : '';
}
// Load items using shared editor
// 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
});
addItem('invoice-items', { item, type: 'invoice', laborRate: qboLaborRate, onUpdate: updateInvoiceTotals });
});
updateInvoiceTotals();
}
@@ -105,39 +119,32 @@ function prepareNewInvoice() {
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();
// Add one default item
addItem('invoice-items', {
type: 'invoice',
laborRate: qboLaborRate,
onUpdate: updateInvoiceTotals
});
addItem('invoice-items', { type: 'invoice', laborRate: qboLaborRate, onUpdate: updateInvoiceTotals });
}
export function addInvoiceItem(item = null) {
addItem('invoice-items', {
item,
type: 'invoice',
laborRate: qboLaborRate,
onUpdate: updateInvoiceTotals
});
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)}`;
@@ -146,6 +153,9 @@ export function updateInvoiceTotals() {
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,
@@ -156,44 +166,27 @@ export async function handleInvoiceSubmit(e) {
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;
}
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 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 (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}`);
@@ -206,16 +199,35 @@ export async function handleInvoiceSubmit(e) {
}
}
// Wire up form submit and tax-exempt checkbox
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'] });
}
}
// Expose for onclick handlers
window.openInvoiceModal = openInvoiceModal;
window.closeInvoiceModal = closeInvoiceModal;
window.addInvoiceItem = addInvoiceItem;

View File

@@ -7,6 +7,21 @@ import { setDefaultDate, showSpinner, hideSpinner } from '../utils/helpers.js';
let currentQuoteId = null;
/**
* 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('quote-tax-exempt');
if (cb) {
cb.checked = (customer.taxable === false);
updateQuoteTotals();
}
}
}
export function openQuoteModal(quoteId = null) {
currentQuoteId = quoteId;
@@ -146,6 +161,17 @@ export function initQuoteModal() {
const taxExempt = document.getElementById('quote-tax-exempt');
if (taxExempt) taxExempt.addEventListener('change', updateQuoteTotals);
// Watch for customer selection → auto-set tax exempt (only for new quotes)
const customerHidden = document.getElementById('quote-customer');
if (customerHidden) {
const observer = new MutationObserver(() => {
if (!currentQuoteId && customerHidden.value) {
applyCustomerTaxStatus(customerHidden.value);
}
});
observer.observe(customerHidden, { attributes: true, attributeFilter: ['value'] });
}
}
// Expose for onclick handlers