From eef316402c04e832fc79ff0e04b06e22b3e8da38 Mon Sep 17 00:00:00 2001 From: Andreas Knuth Date: Mon, 2 Feb 2026 16:15:53 -0600 Subject: [PATCH] last state --- public/app.js | 350 +++++++++++++++++++++++++------- public/index.html | 81 +++++++- server.js | 234 ++++++++++++++++++++- templates/invoice-template.html | 10 + templates/quote-template.html | 10 + 5 files changed, 592 insertions(+), 93 deletions(-) diff --git a/public/app.js b/public/app.js index 35735c2..abed82d 100644 --- a/public/app.js +++ b/public/app.js @@ -8,6 +8,66 @@ 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(); @@ -129,7 +189,6 @@ async function loadCustomers() { const response = await fetch('/api/customers'); customers = await response.json(); renderCustomers(); - updateCustomerDropdown(); } catch (error) { console.error('Error loading customers:', error); alert('Error loading customers'); @@ -151,17 +210,6 @@ function renderCustomers() { `).join(''); } -function updateCustomerDropdown() { - const quoteSelect = document.getElementById('quote-customer'); - const invoiceSelect = document.getElementById('invoice-customer'); - - const options = '' + - customers.map(c => ``).join(''); - - if (quoteSelect) quoteSelect.innerHTML = options; - if (invoiceSelect) invoiceSelect.innerHTML = options; -} - function openCustomerModal(customerId = null) { currentCustomerId = customerId; const modal = document.getElementById('customer-modal'); @@ -289,7 +337,28 @@ async function openQuoteModal(quoteId = null) { 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; @@ -326,40 +395,71 @@ function addQuoteItem(item = null) { const itemsDiv = document.getElementById('quote-items'); const itemDiv = document.createElement('div'); - itemDiv.className = 'grid grid-cols-12 gap-3 items-start mb-3'; + 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}
-
- - -
-
- - -
-
- + + +
+
+
+ + +
+
+ +
+
+
+
+ + +
+
+ + +
+
+ +
+
`; @@ -383,12 +483,13 @@ function addQuoteItem(item = null) { } quill.on('text-change', () => { + updateItemPreview(itemDiv, itemId); updateQuoteTotals(); }); editorDiv.quillInstance = quill; - // Auto-calculate amount + // 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"]'); @@ -399,16 +500,40 @@ function addQuoteItem(item = null) { 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', updateQuoteTotals); + 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(); @@ -548,7 +673,7 @@ async function convertQuoteToInvoice(quoteId) { } } -// Invoice Management +// Invoice Management - Same accordion pattern async function loadInvoices() { try { const response = await fetch('/api/invoices'); @@ -588,6 +713,23 @@ async function openInvoiceModal(invoiceId = null) { 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-customer').value = data.invoice.customer_id; const dateOnly = data.invoice.invoice_date.split('T')[0]; document.getElementById('invoice-date').value = dateOnly; @@ -628,40 +770,71 @@ function addInvoiceItem(item = null) { const itemsDiv = document.getElementById('invoice-items'); const itemDiv = document.createElement('div'); - itemDiv.className = 'grid grid-cols-12 gap-3 items-start mb-3'; + 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}
-
- - -
-
- - -
-
- + + +
+
+
+ + +
+
+ +
+
+
+
+ + +
+
+ + +
+
+ +
+
`; @@ -685,12 +858,13 @@ function addInvoiceItem(item = null) { } quill.on('text-change', () => { + updateInvoiceItemPreview(itemDiv, itemId); updateInvoiceTotals(); }); editorDiv.quillInstance = quill; - // Auto-calculate amount + // 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"]'); @@ -701,16 +875,40 @@ function addInvoiceItem(item = null) { 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', updateInvoiceTotals); + 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(); @@ -822,4 +1020,4 @@ async function deleteInvoice(id) { function viewInvoicePDF(id) { window.open(`/api/invoices/${id}/pdf`, '_blank'); -} +} \ No newline at end of file diff --git a/public/index.html b/public/index.html index 1571047..a8d2d6f 100644 --- a/public/index.html +++ b/public/index.html @@ -7,6 +7,7 @@ + @@ -219,6 +228,7 @@ accounting@bayarea-cc.com
+
INVOICE
diff --git a/templates/quote-template.html b/templates/quote-template.html index b9f3e66..feb9ce4 100644 --- a/templates/quote-template.html +++ b/templates/quote-template.html @@ -29,6 +29,7 @@ margin-bottom: 40px; padding-bottom: 20px; border-bottom: 2px solid #333; + position: relative; } .company-info { @@ -193,6 +194,14 @@ .logo-size { height: 40px; } + .document-type { + font-size: 24px; + font-weight: bold; + margin-bottom: 10px; + color: #333; + position: absolute; + bottom: 0; + } @@ -219,6 +228,7 @@ support@bayarea-cc.com
+
QUOTE