last state
This commit is contained in:
350
public/app.js
350
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 = '<option value="">Select Customer...</option>' +
|
||||
customers.map(c => `<option value="${c.id}">${c.name}</option>`).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 = `
|
||||
<div class="col-span-1">
|
||||
<label class="block text-xs font-medium text-gray-700 mb-1">Qty</label>
|
||||
<input type="text" data-item="${itemId}" data-field="quantity"
|
||||
value="${item ? item.quantity : ''}"
|
||||
class="quote-item-input w-full px-2 py-2 border border-gray-300 rounded-md text-sm">
|
||||
</div>
|
||||
<div class="col-span-5">
|
||||
<label class="block text-xs font-medium text-gray-700 mb-1">Description</label>
|
||||
<div data-item="${itemId}" data-field="description"
|
||||
class="quote-item-description-editor border border-gray-300 rounded-md bg-white"
|
||||
style="min-height: 60px;">
|
||||
<!-- Accordion Header -->
|
||||
<div @click="open = !open" class="flex items-center justify-between p-4 cursor-pointer hover:bg-gray-50">
|
||||
<div class="flex items-center space-x-4 flex-1">
|
||||
<svg x-show="!open" class="w-5 h-5 text-gray-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
<svg x-show="open" class="w-5 h-5 text-gray-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 15l7-7 7 7" />
|
||||
</svg>
|
||||
<span class="text-sm font-medium">Qty: <span class="item-qty-preview">${previewQty}</span></span>
|
||||
<span class="text-sm text-gray-600 flex-1 truncate item-desc-preview">${previewDesc}</span>
|
||||
<span class="text-sm font-semibold item-amount-preview">${previewAmount}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-span-2">
|
||||
<label class="block text-xs font-medium text-gray-700 mb-1">Rate</label>
|
||||
<input type="text" data-item="${itemId}" data-field="rate"
|
||||
value="${item ? item.rate : ''}"
|
||||
class="quote-item-input w-full px-2 py-2 border border-gray-300 rounded-md text-sm">
|
||||
</div>
|
||||
<div class="col-span-2">
|
||||
<label class="block text-xs font-medium text-gray-700 mb-1">Amount</label>
|
||||
<input type="text" data-item="${itemId}" data-field="amount"
|
||||
value="${item ? item.amount : ''}"
|
||||
class="quote-item-amount w-full px-2 py-2 border border-gray-300 rounded-md text-sm">
|
||||
</div>
|
||||
<div class="col-span-1 flex items-end">
|
||||
<button type="button" onclick="removeQuoteItem(${itemId})"
|
||||
class="w-full px-2 py-2 bg-red-500 hover:bg-red-600 text-white rounded-md text-sm">
|
||||
×
|
||||
</button>
|
||||
|
||||
<!-- Accordion Content -->
|
||||
<div x-show="open" x-transition class="p-4 border-t border-gray-200">
|
||||
<div class="grid grid-cols-12 gap-3 items-start">
|
||||
<div class="col-span-1">
|
||||
<label class="block text-xs font-medium text-gray-700 mb-1">Qty</label>
|
||||
<input type="text" data-item="${itemId}" data-field="quantity"
|
||||
value="${item ? item.quantity : ''}"
|
||||
class="quote-item-input w-full px-2 py-2 border border-gray-300 rounded-md text-sm">
|
||||
</div>
|
||||
<div class="col-span-5">
|
||||
<label class="block text-xs font-medium text-gray-700 mb-1">Description</label>
|
||||
<div data-item="${itemId}" data-field="description"
|
||||
class="quote-item-description-editor border border-gray-300 rounded-md bg-white"
|
||||
style="min-height: 60px;">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-span-2">
|
||||
<label class="block text-xs font-medium text-gray-700 mb-1">Rate</label>
|
||||
<input type="text" data-item="${itemId}" data-field="rate"
|
||||
value="${item ? item.rate : ''}"
|
||||
class="quote-item-input w-full px-2 py-2 border border-gray-300 rounded-md text-sm">
|
||||
</div>
|
||||
<div class="col-span-2">
|
||||
<label class="block text-xs font-medium text-gray-700 mb-1">Amount</label>
|
||||
<input type="text" data-item="${itemId}" data-field="amount"
|
||||
value="${item ? item.amount : ''}"
|
||||
class="quote-item-amount w-full px-2 py-2 border border-gray-300 rounded-md text-sm">
|
||||
</div>
|
||||
<div class="col-span-1 flex items-end">
|
||||
<button type="button" onclick="removeQuoteItem(${itemId})"
|
||||
class="w-full px-2 py-2 bg-red-500 hover:bg-red-600 text-white rounded-md text-sm">
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -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 = `
|
||||
<div class="col-span-1">
|
||||
<label class="block text-xs font-medium text-gray-700 mb-1">Qty</label>
|
||||
<input type="text" data-item="${itemId}" data-field="quantity"
|
||||
value="${item ? item.quantity : ''}"
|
||||
class="invoice-item-input w-full px-2 py-2 border border-gray-300 rounded-md text-sm">
|
||||
</div>
|
||||
<div class="col-span-5">
|
||||
<label class="block text-xs font-medium text-gray-700 mb-1">Description</label>
|
||||
<div data-item="${itemId}" data-field="description"
|
||||
class="invoice-item-description-editor border border-gray-300 rounded-md bg-white"
|
||||
style="min-height: 60px;">
|
||||
<!-- Accordion Header -->
|
||||
<div @click="open = !open" class="flex items-center justify-between p-4 cursor-pointer hover:bg-gray-50">
|
||||
<div class="flex items-center space-x-4 flex-1">
|
||||
<svg x-show="!open" class="w-5 h-5 text-gray-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
<svg x-show="open" class="w-5 h-5 text-gray-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 15l7-7 7 7" />
|
||||
</svg>
|
||||
<span class="text-sm font-medium">Qty: <span class="item-qty-preview">${previewQty}</span></span>
|
||||
<span class="text-sm text-gray-600 flex-1 truncate item-desc-preview">${previewDesc}</span>
|
||||
<span class="text-sm font-semibold item-amount-preview">${previewAmount}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-span-2">
|
||||
<label class="block text-xs font-medium text-gray-700 mb-1">Rate</label>
|
||||
<input type="text" data-item="${itemId}" data-field="rate"
|
||||
value="${item ? item.rate : ''}"
|
||||
class="invoice-item-input w-full px-2 py-2 border border-gray-300 rounded-md text-sm">
|
||||
</div>
|
||||
<div class="col-span-2">
|
||||
<label class="block text-xs font-medium text-gray-700 mb-1">Amount</label>
|
||||
<input type="text" data-item="${itemId}" data-field="amount"
|
||||
value="${item ? item.amount : ''}"
|
||||
class="invoice-item-amount w-full px-2 py-2 border border-gray-300 rounded-md text-sm">
|
||||
</div>
|
||||
<div class="col-span-1 flex items-end">
|
||||
<button type="button" onclick="removeInvoiceItem(${itemId})"
|
||||
class="w-full px-2 py-2 bg-red-500 hover:bg-red-600 text-white rounded-md text-sm">
|
||||
×
|
||||
</button>
|
||||
|
||||
<!-- Accordion Content -->
|
||||
<div x-show="open" x-transition class="p-4 border-t border-gray-200">
|
||||
<div class="grid grid-cols-12 gap-3 items-start">
|
||||
<div class="col-span-1">
|
||||
<label class="block text-xs font-medium text-gray-700 mb-1">Qty</label>
|
||||
<input type="text" data-item="${itemId}" data-field="quantity"
|
||||
value="${item ? item.quantity : ''}"
|
||||
class="invoice-item-input w-full px-2 py-2 border border-gray-300 rounded-md text-sm">
|
||||
</div>
|
||||
<div class="col-span-5">
|
||||
<label class="block text-xs font-medium text-gray-700 mb-1">Description</label>
|
||||
<div data-item="${itemId}" data-field="description"
|
||||
class="invoice-item-description-editor border border-gray-300 rounded-md bg-white"
|
||||
style="min-height: 60px;">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-span-2">
|
||||
<label class="block text-xs font-medium text-gray-700 mb-1">Rate</label>
|
||||
<input type="text" data-item="${itemId}" data-field="rate"
|
||||
value="${item ? item.rate : ''}"
|
||||
class="invoice-item-input w-full px-2 py-2 border border-gray-300 rounded-md text-sm">
|
||||
</div>
|
||||
<div class="col-span-2">
|
||||
<label class="block text-xs font-medium text-gray-700 mb-1">Amount</label>
|
||||
<input type="text" data-item="${itemId}" data-field="amount"
|
||||
value="${item ? item.amount : ''}"
|
||||
class="invoice-item-amount w-full px-2 py-2 border border-gray-300 rounded-md text-sm">
|
||||
</div>
|
||||
<div class="col-span-1 flex items-end">
|
||||
<button type="button" onclick="removeInvoiceItem(${itemId})"
|
||||
class="w-full px-2 py-2 bg-red-500 hover:bg-red-600 text-white rounded-md text-sm">
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<link href="https://cdn.quilljs.com/1.3.6/quill.snow.css" rel="stylesheet">
|
||||
<script src="https://cdn.quilljs.com/1.3.6/quill.js"></script>
|
||||
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
||||
<style>
|
||||
.modal {
|
||||
display: none;
|
||||
@@ -231,12 +232,41 @@
|
||||
|
||||
<form id="quote-form" class="space-y-6">
|
||||
<div class="grid grid-cols-3 gap-4">
|
||||
<div>
|
||||
<div x-data="customerSearch('quote')" class="relative">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Customer</label>
|
||||
<select id="quote-customer" required
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500">
|
||||
<option value="">Select Customer...</option>
|
||||
</select>
|
||||
<div class="relative">
|
||||
<input
|
||||
type="text"
|
||||
x-model="search"
|
||||
@click="open = true"
|
||||
@focus="open = true"
|
||||
@keydown.escape="open = false"
|
||||
@keydown.arrow-down.prevent="highlightNext()"
|
||||
@keydown.arrow-up.prevent="highlightPrev()"
|
||||
@keydown.enter.prevent="selectHighlighted()"
|
||||
placeholder="Search customer..."
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500"
|
||||
/>
|
||||
<input type="hidden" id="quote-customer" :value="selectedId" required>
|
||||
|
||||
<div
|
||||
x-show="open && filteredCustomers.length > 0"
|
||||
@click.away="open = false"
|
||||
x-transition
|
||||
class="absolute z-50 w-full mt-1 bg-white border border-gray-300 rounded-md shadow-lg max-h-60 overflow-auto"
|
||||
>
|
||||
<template x-for="(customer, index) in filteredCustomers" :key="customer.id">
|
||||
<div
|
||||
@click="selectCustomer(customer)"
|
||||
:class="{'bg-blue-100': index === highlighted, 'hover:bg-gray-100': index !== highlighted}"
|
||||
class="px-4 py-2 cursor-pointer text-sm"
|
||||
>
|
||||
<div class="font-medium" x-text="customer.name"></div>
|
||||
<div class="text-xs text-gray-500" x-text="customer.city + ', ' + customer.state"></div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Date</label>
|
||||
@@ -306,12 +336,41 @@
|
||||
|
||||
<form id="invoice-form" class="space-y-6">
|
||||
<div class="grid grid-cols-4 gap-4">
|
||||
<div>
|
||||
<div x-data="customerSearch('invoice')" class="relative">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Customer</label>
|
||||
<select id="invoice-customer" required
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500">
|
||||
<option value="">Select Customer...</option>
|
||||
</select>
|
||||
<div class="relative">
|
||||
<input
|
||||
type="text"
|
||||
x-model="search"
|
||||
@click="open = true"
|
||||
@focus="open = true"
|
||||
@keydown.escape="open = false"
|
||||
@keydown.arrow-down.prevent="highlightNext()"
|
||||
@keydown.arrow-up.prevent="highlightPrev()"
|
||||
@keydown.enter.prevent="selectHighlighted()"
|
||||
placeholder="Search customer..."
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500"
|
||||
/>
|
||||
<input type="hidden" id="invoice-customer" :value="selectedId" required>
|
||||
|
||||
<div
|
||||
x-show="open && filteredCustomers.length > 0"
|
||||
@click.away="open = false"
|
||||
x-transition
|
||||
class="absolute z-50 w-full mt-1 bg-white border border-gray-300 rounded-md shadow-lg max-h-60 overflow-auto"
|
||||
>
|
||||
<template x-for="(customer, index) in filteredCustomers" :key="customer.id">
|
||||
<div
|
||||
@click="selectCustomer(customer)"
|
||||
:class="{'bg-blue-100': index === highlighted, 'hover:bg-gray-100': index !== highlighted}"
|
||||
class="px-4 py-2 cursor-pointer text-sm"
|
||||
>
|
||||
<div class="font-medium" x-text="customer.name"></div>
|
||||
<div class="text-xs text-gray-500" x-text="customer.city + ', ' + customer.state"></div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Date</label>
|
||||
@@ -381,4 +440,4 @@
|
||||
|
||||
<script src="app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
Reference in New Issue
Block a user