// Global state
let customers = [];
let quotes = [];
let invoices = [];
let currentQuoteId = null;
let currentInvoiceId = null;
let currentCustomerId = null;
let itemCounter = 0;
let currentLogoFile = null;
// Alpine.js Customer Search Component
function customerSearch(type) {
return {
search: '',
selectedId: '',
selectedName: '',
open: false,
highlighted: 0,
get filteredCustomers() {
if (!this.search) {
return customers.slice(0, 50); // Show first 50 if no search
}
const searchLower = this.search.toLowerCase();
return customers.filter(c =>
c.name.toLowerCase().includes(searchLower) ||
c.city.toLowerCase().includes(searchLower) ||
(c.account_number && c.account_number.includes(searchLower))
).slice(0, 50); // Limit to 50 results
},
selectCustomer(customer) {
this.selectedId = customer.id;
this.selectedName = customer.name;
this.search = customer.name;
this.open = false;
this.highlighted = 0;
},
highlightNext() {
if (this.highlighted < this.filteredCustomers.length - 1) {
this.highlighted++;
}
},
highlightPrev() {
if (this.highlighted > 0) {
this.highlighted--;
}
},
selectHighlighted() {
if (this.filteredCustomers[this.highlighted]) {
this.selectCustomer(this.filteredCustomers[this.highlighted]);
}
},
reset() {
this.search = '';
this.selectedId = '';
this.selectedName = '';
this.open = false;
this.highlighted = 0;
}
};
}
// Make it globally available for Alpine
window.customerSearch = customerSearch;
// Initialize app
document.addEventListener('DOMContentLoaded', () => {
loadCustomers();
loadQuotes();
loadInvoices();
setDefaultDate();
checkCurrentLogo();
// Setup form handlers
document.getElementById('customer-form').addEventListener('submit', handleCustomerSubmit);
document.getElementById('quote-form').addEventListener('submit', handleQuoteSubmit);
document.getElementById('invoice-form').addEventListener('submit', handleInvoiceSubmit);
document.getElementById('quote-tax-exempt').addEventListener('change', updateQuoteTotals);
document.getElementById('invoice-tax-exempt').addEventListener('change', updateInvoiceTotals);
// Setup logo upload handler
document.getElementById('logo-upload').addEventListener('change', (e) => {
const file = e.target.files[0];
if (file) {
currentLogoFile = file;
document.getElementById('logo-filename').textContent = file.name;
document.getElementById('upload-btn').disabled = false;
}
});
});
// Tab Management
function showTab(tabName) {
document.querySelectorAll('.tab-content').forEach(tab => tab.classList.add('hidden'));
document.querySelectorAll('.tab-btn').forEach(btn => btn.classList.remove('bg-blue-800'));
document.getElementById(`${tabName}-tab`).classList.remove('hidden');
document.getElementById(`tab-${tabName}`).classList.add('bg-blue-800');
if (tabName === 'quotes') {
loadQuotes();
} else if (tabName === 'invoices') {
loadInvoices();
} else if (tabName === 'customers') {
loadCustomers();
} else if (tabName === 'settings') {
checkCurrentLogo();
}
}
// Date helper
function setDefaultDate() {
const today = new Date().toISOString().split('T')[0];
const quoteDateEl = document.getElementById('quote-date');
const invoiceDateEl = document.getElementById('invoice-date');
if (quoteDateEl) quoteDateEl.value = today;
if (invoiceDateEl) invoiceDateEl.value = today;
}
function formatDate(date) {
const d = new Date(date);
const month = String(d.getMonth() + 1).padStart(2, '0');
const day = String(d.getDate()).padStart(2, '0');
const year = d.getFullYear();
return `${month}/${day}/${year}`;
}
// Logo Management
async function checkCurrentLogo() {
try {
const response = await fetch('/api/logo-info');
if (response.ok) {
const data = await response.json();
if (data.hasLogo) {
document.getElementById('logo-preview').classList.remove('hidden');
document.getElementById('logo-image').src = data.logoPath + '?t=' + Date.now();
}
}
} catch (error) {
console.error('Error checking logo:', error);
}
}
async function uploadLogo() {
if (!currentLogoFile) {
alert('Please select a file first');
return;
}
const formData = new FormData();
formData.append('logo', currentLogoFile);
const statusDiv = document.getElementById('upload-status');
statusDiv.innerHTML = '
Uploading...
';
try {
const response = await fetch('/api/upload-logo', {
method: 'POST',
body: formData
});
if (response.ok) {
const data = await response.json();
statusDiv.innerHTML = '✓ Logo uploaded successfully!
';
document.getElementById('logo-preview').classList.remove('hidden');
document.getElementById('logo-image').src = data.path + '?t=' + Date.now();
document.getElementById('upload-btn').disabled = true;
currentLogoFile = null;
document.getElementById('logo-filename').textContent = '';
document.getElementById('logo-upload').value = '';
} else {
const error = await response.json();
statusDiv.innerHTML = `✗ Error: ${error.error}
`;
}
} catch (error) {
console.error('Upload error:', error);
statusDiv.innerHTML = '✗ Upload failed
';
}
}
// Customer Management
async function loadCustomers() {
try {
const response = await fetch('/api/customers');
customers = await response.json();
renderCustomers();
} catch (error) {
console.error('Error loading customers:', error);
alert('Error loading customers');
}
}
function renderCustomers() {
const tbody = document.getElementById('customers-list');
tbody.innerHTML = customers.map(customer => `
| ${customer.name} |
${customer.street}, ${customer.city}, ${customer.state} ${customer.zip_code} |
${customer.account_number || '-'} |
|
`).join('');
}
function openCustomerModal(customerId = null) {
currentCustomerId = customerId;
const modal = document.getElementById('customer-modal');
const title = document.getElementById('customer-modal-title');
if (customerId) {
title.textContent = 'Edit Customer';
const customer = customers.find(c => c.id === customerId);
document.getElementById('customer-id').value = customer.id;
document.getElementById('customer-name').value = customer.name;
document.getElementById('customer-street').value = customer.street;
document.getElementById('customer-city').value = customer.city;
document.getElementById('customer-state').value = customer.state;
document.getElementById('customer-zip').value = customer.zip_code;
document.getElementById('customer-account').value = customer.account_number || '';
} else {
title.textContent = 'New Customer';
document.getElementById('customer-form').reset();
document.getElementById('customer-id').value = '';
}
modal.classList.add('active');
}
function closeCustomerModal() {
document.getElementById('customer-modal').classList.remove('active');
currentCustomerId = null;
}
async function handleCustomerSubmit(e) {
e.preventDefault();
const data = {
name: document.getElementById('customer-name').value,
street: document.getElementById('customer-street').value,
city: document.getElementById('customer-city').value,
state: document.getElementById('customer-state').value.toUpperCase(),
zip_code: document.getElementById('customer-zip').value,
account_number: document.getElementById('customer-account').value
};
try {
const customerId = document.getElementById('customer-id').value;
const url = customerId ? `/api/customers/${customerId}` : '/api/customers';
const method = customerId ? 'PUT' : 'POST';
const response = await fetch(url, {
method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (response.ok) {
closeCustomerModal();
loadCustomers();
} else {
alert('Error saving customer');
}
} catch (error) {
console.error('Error:', error);
alert('Error saving customer');
}
}
async function editCustomer(id) {
openCustomerModal(id);
}
async function deleteCustomer(id) {
if (!confirm('Are you sure you want to delete this customer?')) return;
try {
const response = await fetch(`/api/customers/${id}`, { method: 'DELETE' });
if (response.ok) {
loadCustomers();
} else {
alert('Error deleting customer');
}
} catch (error) {
console.error('Error:', error);
alert('Error deleting customer');
}
}
// Quote Management
async function loadQuotes() {
try {
const response = await fetch('/api/quotes');
quotes = await response.json();
renderQuotes();
} catch (error) {
console.error('Error loading quotes:', error);
alert('Error loading quotes');
}
}
function renderQuotes() {
const tbody = document.getElementById('quotes-list');
tbody.innerHTML = quotes.map(quote => {
const total = quote.has_tbd ? `$${parseFloat(quote.total).toFixed(2)}*` : `$${parseFloat(quote.total).toFixed(2)}`;
return `
| ${quote.quote_number} |
${quote.customer_name || 'N/A'} |
${formatDate(quote.quote_date)} |
${total} |
|
`;
}).join('');
}
async function openQuoteModal(quoteId = null) {
currentQuoteId = quoteId;
const modal = document.getElementById('quote-modal');
const title = document.getElementById('quote-modal-title');
if (quoteId) {
title.textContent = 'Edit Quote';
const response = await fetch(`/api/quotes/${quoteId}`);
const data = await response.json();
// Set customer in Alpine component
const customer = customers.find(c => c.id === data.quote.customer_id);
if (customer) {
// Find the Alpine component and update it
const customerInput = document.querySelector('#quote-modal input[placeholder="Search customer..."]');
if (customerInput) {
// Trigger Alpine to update
customerInput.value = customer.name;
customerInput.dispatchEvent(new Event('input'));
// Set the values directly on the Alpine component
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;
const dateOnly = data.quote.quote_date.split('T')[0];
document.getElementById('quote-date').value = dateOnly;
document.getElementById('quote-tax-exempt').checked = data.quote.tax_exempt;
// Load items
document.getElementById('quote-items').innerHTML = '';
itemCounter = 0;
data.items.forEach(item => {
addQuoteItem(item);
});
updateQuoteTotals();
} else {
title.textContent = 'New Quote';
document.getElementById('quote-form').reset();
document.getElementById('quote-items').innerHTML = '';
itemCounter = 0;
setDefaultDate();
// Add one default item
addQuoteItem();
}
modal.classList.add('active');
}
function closeQuoteModal() {
document.getElementById('quote-modal').classList.remove('active');
currentQuoteId = null;
}
function addQuoteItem(item = null) {
const itemId = itemCounter++;
const itemsDiv = document.getElementById('quote-items');
const itemDiv = document.createElement('div');
itemDiv.className = 'border border-gray-300 rounded-lg mb-3 bg-white';
itemDiv.id = `quote-item-${itemId}`;
itemDiv.setAttribute('x-data', `{ open: ${item ? 'false' : 'true'} }`);
// Get preview text
const previewQty = item ? item.quantity : '';
const previewAmount = item ? item.amount : '$0.00';
let previewDesc = 'New item';
if (item && item.description) {
const temp = document.createElement('div');
temp.innerHTML = item.description;
previewDesc = temp.textContent.substring(0, 60) + (temp.textContent.length > 60 ? '...' : '');
}
itemDiv.innerHTML = `
Qty: ${previewQty}
${previewDesc}
${previewAmount}
`;
itemsDiv.appendChild(itemDiv);
// Initialize Quill editor
const editorDiv = itemDiv.querySelector('.quote-item-description-editor');
const quill = new Quill(editorDiv, {
theme: 'snow',
modules: {
toolbar: [
['bold', 'italic', 'underline'],
[{ 'list': 'ordered'}, { 'list': 'bullet' }],
['clean']
]
}
});
if (item && item.description) {
quill.root.innerHTML = item.description;
}
quill.on('text-change', () => {
updateItemPreview(itemDiv, itemId);
updateQuoteTotals();
});
editorDiv.quillInstance = quill;
// Auto-calculate amount and update preview
const qtyInput = itemDiv.querySelector('[data-field="quantity"]');
const rateInput = itemDiv.querySelector('[data-field="rate"]');
const amountInput = itemDiv.querySelector('[data-field="amount"]');
const calculateAmount = () => {
if (qtyInput.value && rateInput.value && rateInput.value.toUpperCase() !== 'TBD') {
const qty = parseFloat(qtyInput.value) || 0;
const rateValue = parseFloat(rateInput.value.replace(/[^0-9.]/g, '')) || 0;
amountInput.value = (qty * rateValue).toFixed(2);
}
updateItemPreview(itemDiv, itemId);
updateQuoteTotals();
};
qtyInput.addEventListener('input', calculateAmount);
rateInput.addEventListener('input', calculateAmount);
amountInput.addEventListener('input', () => {
updateItemPreview(itemDiv, itemId);
updateQuoteTotals();
});
updateItemPreview(itemDiv, itemId);
updateQuoteTotals();
}
function updateItemPreview(itemDiv, itemId) {
const qtyInput = itemDiv.querySelector('[data-field="quantity"]');
const amountInput = itemDiv.querySelector('[data-field="amount"]');
const editorDiv = itemDiv.querySelector('.quote-item-description-editor');
const qtyPreview = itemDiv.querySelector('.item-qty-preview');
const descPreview = itemDiv.querySelector('.item-desc-preview');
const amountPreview = itemDiv.querySelector('.item-amount-preview');
if (qtyPreview) qtyPreview.textContent = qtyInput.value || '0';
if (amountPreview) amountPreview.textContent = amountInput.value || '$0.00';
if (descPreview && editorDiv.quillInstance) {
const plainText = editorDiv.quillInstance.getText().trim();
const preview = plainText.substring(0, 60) + (plainText.length > 60 ? '...' : '');
descPreview.textContent = preview || 'New item';
}
}
function removeQuoteItem(itemId) {
document.getElementById(`quote-item-${itemId}`).remove();
updateQuoteTotals();
}
function moveQuoteItemUp(itemId) {
const item = document.getElementById(`quote-item-${itemId}`);
const prevItem = item.previousElementSibling;
if (prevItem) {
item.parentNode.insertBefore(item, prevItem);
updateQuoteTotals();
}
}
function moveQuoteItemDown(itemId) {
const item = document.getElementById(`quote-item-${itemId}`);
const nextItem = item.nextElementSibling;
if (nextItem) {
item.parentNode.insertBefore(nextItem, item);
updateQuoteTotals();
}
}
function updateQuoteTotals() {
const items = getQuoteItems();
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';
}
function getQuoteItems() {
const items = [];
const itemDivs = document.querySelectorAll('#quote-items > div');
itemDivs.forEach(div => {
const descEditor = div.querySelector('.quote-item-description-editor');
const descriptionHTML = descEditor && descEditor.quillInstance
? descEditor.quillInstance.root.innerHTML
: '';
const item = {
quantity: div.querySelector('[data-field="quantity"]').value,
description: descriptionHTML,
rate: div.querySelector('[data-field="rate"]').value,
amount: div.querySelector('[data-field="amount"]').value
};
items.push(item);
});
return items;
}
async function handleQuoteSubmit(e) {
e.preventDefault();
const items = getQuoteItems();
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();
loadQuotes();
} else {
alert('Error saving quote');
}
} catch (error) {
console.error('Error:', error);
alert('Error saving quote');
}
}
async function editQuote(id) {
await openQuoteModal(id);
}
async function deleteQuote(id) {
if (!confirm('Are you sure you want to delete this quote?')) return;
try {
const response = await fetch(`/api/quotes/${id}`, { method: 'DELETE' });
if (response.ok) {
loadQuotes();
} else {
alert('Error deleting quote');
}
} catch (error) {
console.error('Error:', error);
alert('Error deleting quote');
}
}
function viewQuotePDF(id) {
window.open(`/api/quotes/${id}/pdf`, '_blank');
}
async function convertQuoteToInvoice(quoteId) {
if (!confirm('Convert this quote to an invoice?')) return;
try {
const response = await fetch(`/api/quotes/${quoteId}/convert-to-invoice`, {
method: 'POST'
});
if (response.ok) {
const invoice = await response.json();
alert(`Invoice ${invoice.invoice_number} created successfully!`);
loadInvoices();
showTab('invoices');
} else {
const error = await response.json();
alert(error.error || 'Error converting quote to invoice');
}
} catch (error) {
console.error('Error:', error);
alert('Error converting quote to invoice');
}
}
// Invoice Management - Same accordion pattern
async function fetchNextInvoiceNumber() {
try {
const response = await fetch('/api/invoices/next-number');
const data = await response.json();
document.getElementById('invoice-number').value = data.next_number;
} catch (error) {
console.error('Error fetching next invoice number:', error);
}
}
async function loadInvoices() {
try {
const response = await fetch('/api/invoices');
invoices = await response.json();
renderInvoices();
} catch (error) {
console.error('Error loading invoices:', error);
alert('Error loading invoices');
}
}
function renderInvoices() {
const tbody = document.getElementById('invoices-list');
tbody.innerHTML = invoices.map(invoice => `
| ${invoice.invoice_number} |
${invoice.customer_name || 'N/A'} |
${formatDate(invoice.invoice_date)} |
${invoice.terms} |
$${parseFloat(invoice.total).toFixed(2)} |
|
`).join('');
}
async function openInvoiceModal(invoiceId = null) {
currentInvoiceId = invoiceId;
const modal = document.getElementById('invoice-modal');
const title = document.getElementById('invoice-modal-title');
if (invoiceId) {
title.textContent = 'Edit Invoice';
const response = await fetch(`/api/invoices/${invoiceId}`);
const data = await response.json();
// Set customer in Alpine component
const customer = customers.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;
const dateOnly = data.invoice.invoice_date.split('T')[0];
document.getElementById('invoice-date').value = dateOnly;
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;
// Load items
document.getElementById('invoice-items').innerHTML = '';
itemCounter = 0;
data.items.forEach(item => {
addInvoiceItem(item);
});
updateInvoiceTotals();
} else {
title.textContent = 'New Invoice';
document.getElementById('invoice-form').reset();
document.getElementById('invoice-items').innerHTML = '';
document.getElementById('invoice-terms').value = 'Net 30';
itemCounter = 0;
setDefaultDate();
// Fetch next invoice number
fetchNextInvoiceNumber();
// Add one default item
addInvoiceItem();
}
modal.classList.add('active');
}
function closeInvoiceModal() {
document.getElementById('invoice-modal').classList.remove('active');
currentInvoiceId = null;
}
function addInvoiceItem(item = null) {
const itemId = itemCounter++;
const itemsDiv = document.getElementById('invoice-items');
const itemDiv = document.createElement('div');
itemDiv.className = 'border border-gray-300 rounded-lg mb-3 bg-white';
itemDiv.id = `invoice-item-${itemId}`;
itemDiv.setAttribute('x-data', `{ open: ${item ? 'false' : 'true'} }`);
// Get preview text
const previewQty = item ? item.quantity : '';
const previewAmount = item ? item.amount : '$0.00';
let previewDesc = 'New item';
if (item && item.description) {
const temp = document.createElement('div');
temp.innerHTML = item.description;
previewDesc = temp.textContent.substring(0, 60) + (temp.textContent.length > 60 ? '...' : '');
}
itemDiv.innerHTML = `
Qty: ${previewQty}
${previewDesc}
${previewAmount}
`;
itemsDiv.appendChild(itemDiv);
// Initialize Quill editor
const editorDiv = itemDiv.querySelector('.invoice-item-description-editor');
const quill = new Quill(editorDiv, {
theme: 'snow',
modules: {
toolbar: [
['bold', 'italic', 'underline'],
[{ 'list': 'ordered'}, { 'list': 'bullet' }],
['clean']
]
}
});
if (item && item.description) {
quill.root.innerHTML = item.description;
}
quill.on('text-change', () => {
updateInvoiceItemPreview(itemDiv, itemId);
updateInvoiceTotals();
});
editorDiv.quillInstance = quill;
// Auto-calculate amount and update preview
const qtyInput = itemDiv.querySelector('[data-field="quantity"]');
const rateInput = itemDiv.querySelector('[data-field="rate"]');
const amountInput = itemDiv.querySelector('[data-field="amount"]');
const calculateAmount = () => {
if (qtyInput.value && rateInput.value) {
const qty = parseFloat(qtyInput.value) || 0;
const rateValue = parseFloat(rateInput.value.replace(/[^0-9.]/g, '')) || 0;
amountInput.value = (qty * rateValue).toFixed(2);
}
updateInvoiceItemPreview(itemDiv, itemId);
updateInvoiceTotals();
};
qtyInput.addEventListener('input', calculateAmount);
rateInput.addEventListener('input', calculateAmount);
amountInput.addEventListener('input', () => {
updateInvoiceItemPreview(itemDiv, itemId);
updateInvoiceTotals();
});
updateInvoiceItemPreview(itemDiv, itemId);
updateInvoiceTotals();
}
function updateInvoiceItemPreview(itemDiv, itemId) {
const qtyInput = itemDiv.querySelector('[data-field="quantity"]');
const amountInput = itemDiv.querySelector('[data-field="amount"]');
const editorDiv = itemDiv.querySelector('.invoice-item-description-editor');
const qtyPreview = itemDiv.querySelector('.item-qty-preview');
const descPreview = itemDiv.querySelector('.item-desc-preview');
const amountPreview = itemDiv.querySelector('.item-amount-preview');
if (qtyPreview) qtyPreview.textContent = qtyInput.value || '0';
if (amountPreview) amountPreview.textContent = amountInput.value || '$0.00';
if (descPreview && editorDiv.quillInstance) {
const plainText = editorDiv.quillInstance.getText().trim();
const preview = plainText.substring(0, 60) + (plainText.length > 60 ? '...' : '');
descPreview.textContent = preview || 'New item';
}
}
function removeInvoiceItem(itemId) {
document.getElementById(`invoice-item-${itemId}`).remove();
updateInvoiceTotals();
}
function moveInvoiceItemUp(itemId) {
const item = document.getElementById(`invoice-item-${itemId}`);
const prevItem = item.previousElementSibling;
if (prevItem) {
item.parentNode.insertBefore(item, prevItem);
updateInvoiceTotals();
}
}
function moveInvoiceItemDown(itemId) {
const item = document.getElementById(`invoice-item-${itemId}`);
const nextItem = item.nextElementSibling;
if (nextItem) {
item.parentNode.insertBefore(nextItem, item);
updateInvoiceTotals();
}
}
function updateInvoiceTotals() {
const items = getInvoiceItems();
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';
}
function getInvoiceItems() {
const items = [];
const itemDivs = document.querySelectorAll('#invoice-items > div');
itemDivs.forEach(div => {
const descEditor = div.querySelector('.invoice-item-description-editor');
const descriptionHTML = descEditor && descEditor.quillInstance
? descEditor.quillInstance.root.innerHTML
: '';
const item = {
quantity: div.querySelector('[data-field="quantity"]').value,
description: descriptionHTML,
rate: div.querySelector('[data-field="rate"]').value,
amount: div.querySelector('[data-field="amount"]').value
};
items.push(item);
});
return items;
}
async function handleInvoiceSubmit(e) {
e.preventDefault();
const items = getInvoiceItems();
if (items.length === 0) {
alert('Please add at least one item');
return;
}
const invoiceNumber = document.getElementById('invoice-number').value;
if (!invoiceNumber || !/^\d+$/.test(invoiceNumber)) {
alert('Invalid invoice number. Must be a numeric value.');
return;
}
const data = {
invoice_number: invoiceNumber,
customer_id: parseInt(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,
items: items
};
try {
const url = currentInvoiceId ? `/api/invoices/${currentInvoiceId}` : '/api/invoices';
const method = currentInvoiceId ? 'PUT' : 'POST';
const response = await fetch(url, {
method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (response.ok) {
closeInvoiceModal();
loadInvoices();
} else {
const error = await response.json();
alert(error.error || 'Error saving invoice');
}
} catch (error) {
console.error('Error:', error);
alert('Error saving invoice');
}
}
async function editInvoice(id) {
await openInvoiceModal(id);
}
async function deleteInvoice(id) {
if (!confirm('Are you sure you want to delete this invoice?')) return;
try {
const response = await fetch(`/api/invoices/${id}`, { method: 'DELETE' });
if (response.ok) {
loadInvoices();
} else {
alert('Error deleting invoice');
}
} catch (error) {
console.error('Error:', error);
alert('Error deleting invoice');
}
}
function viewInvoicePDF(id) {
window.open(`/api/invoices/${id}/pdf`, '_blank');
}