invoice-system/public/js/modals/quote-modal.js

180 lines
6.1 KiB
JavaScript

/**
* quote-modal.js — Quote create/edit modal
* Uses shared item-editor for accordion items
*/
import { addItem, getItems, resetItemCounter } from '../utils/item-editor.js';
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;
if (quoteId) {
loadQuoteForEdit(quoteId);
} else {
prepareNewQuote();
}
document.getElementById('quote-modal').classList.add('active');
}
export function closeQuoteModal() {
document.getElementById('quote-modal').classList.remove('active');
currentQuoteId = null;
}
async function loadQuoteForEdit(quoteId) {
document.getElementById('quote-modal-title').textContent = 'Edit Quote';
const response = await fetch(`/api/quotes/${quoteId}`);
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.quote.customer_id);
if (customer) {
const customerInput = document.querySelector('#quote-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('quote-customer').value = data.quote.customer_id;
document.getElementById('quote-date').value = data.quote.quote_date.split('T')[0];
document.getElementById('quote-tax-exempt').checked = data.quote.tax_exempt;
// Load items using shared editor
document.getElementById('quote-items').innerHTML = '';
resetItemCounter();
data.items.forEach(item => {
addItem('quote-items', { item, type: 'quote', onUpdate: updateQuoteTotals });
});
updateQuoteTotals();
}
function prepareNewQuote() {
document.getElementById('quote-modal-title').textContent = 'New Quote';
document.getElementById('quote-form').reset();
document.getElementById('quote-items').innerHTML = '';
resetItemCounter();
setDefaultDate();
// Add one default item
addItem('quote-items', { type: 'quote', onUpdate: updateQuoteTotals });
}
export function addQuoteItem(item = null) {
addItem('quote-items', { item, type: 'quote', onUpdate: updateQuoteTotals });
}
export function updateQuoteTotals() {
const items = getItems('quote-items');
const taxExempt = document.getElementById('quote-tax-exempt').checked;
let subtotal = 0;
let hasTbd = false;
items.forEach(item => {
if (item.rate.toUpperCase() === 'TBD' || item.amount.toUpperCase() === 'TBD') {
hasTbd = true;
} else {
const amount = parseFloat(item.amount.replace(/[$,]/g, '')) || 0;
subtotal += amount;
}
});
const taxAmount = taxExempt ? 0 : (subtotal * 8.25 / 100);
const total = subtotal + taxAmount;
document.getElementById('quote-subtotal').textContent = `$${subtotal.toFixed(2)}`;
document.getElementById('quote-tax').textContent = taxExempt ? '$0.00' : `$${taxAmount.toFixed(2)}`;
document.getElementById('quote-total').textContent = hasTbd ? `$${total.toFixed(2)}*` : `$${total.toFixed(2)}`;
document.getElementById('quote-tax-row').style.display = taxExempt ? 'none' : 'block';
}
export async function handleQuoteSubmit(e) {
e.preventDefault();
const items = getItems('quote-items');
if (items.length === 0) {
alert('Please add at least one item');
return;
}
const data = {
customer_id: parseInt(document.getElementById('quote-customer').value),
quote_date: document.getElementById('quote-date').value,
tax_exempt: document.getElementById('quote-tax-exempt').checked,
items: items
};
try {
const url = currentQuoteId ? `/api/quotes/${currentQuoteId}` : '/api/quotes';
const method = currentQuoteId ? 'PUT' : 'POST';
const response = await fetch(url, {
method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (response.ok) {
closeQuoteModal();
if (window.quoteView) window.quoteView.loadQuotes();
} else {
alert('Error saving quote');
}
} catch (error) {
console.error('Error:', error);
alert('Error saving quote');
}
}
// Wire up form submit and tax-exempt checkbox
export function initQuoteModal() {
const form = document.getElementById('quote-form');
if (form) form.addEventListener('submit', handleQuoteSubmit);
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
window.openQuoteModal = openQuoteModal;
window.closeQuoteModal = closeQuoteModal;
window.addQuoteItem = addQuoteItem;