klapper
This commit is contained in:
parent
209292a61a
commit
f892450914
150
public/app.js
150
public/app.js
|
|
@ -306,47 +306,71 @@ function addQuoteItem(item = null) {
|
||||||
const itemsDiv = document.getElementById('quote-items');
|
const itemsDiv = document.getElementById('quote-items');
|
||||||
|
|
||||||
const itemDiv = document.createElement('div');
|
const itemDiv = document.createElement('div');
|
||||||
itemDiv.className = 'grid grid-cols-12 gap-3 items-start';
|
itemDiv.className = 'border border-gray-300 rounded-lg mb-3';
|
||||||
itemDiv.id = `item-${itemId}`;
|
itemDiv.id = `item-${itemId}`;
|
||||||
|
|
||||||
|
// Create summary text
|
||||||
|
const summaryQty = item ? item.quantity : '';
|
||||||
|
const summaryDesc = item ? (item.description.replace(/<[^>]*>/g, '').substring(0, 50) + '...') : 'New item';
|
||||||
|
const summaryAmount = item ? item.amount : '';
|
||||||
|
|
||||||
itemDiv.innerHTML = `
|
itemDiv.innerHTML = `
|
||||||
<div class="col-span-1">
|
<!-- Summary Header (always visible) -->
|
||||||
<label class="block text-xs font-medium text-gray-700 mb-1">Qty</label>
|
<div class="item-header bg-gray-50 px-4 py-3 cursor-pointer flex items-center justify-between" onclick="toggleItem(${itemId})">
|
||||||
<input type="text" data-item="${itemId}" data-field="quantity"
|
<div class="flex items-center space-x-4 flex-1">
|
||||||
value="${item ? item.quantity : ''}"
|
<span class="text-sm font-medium text-gray-700">Qty: <span class="item-summary-qty">${summaryQty}</span></span>
|
||||||
class="item-input w-full px-2 py-2 border border-gray-300 rounded-md text-sm focus:ring-blue-500 focus:border-blue-500">
|
<span class="text-sm text-gray-600 flex-1 truncate item-summary-desc">${summaryDesc}</span>
|
||||||
</div>
|
<span class="text-sm font-medium text-gray-900">$<span class="item-summary-amount">${summaryAmount}</span></span>
|
||||||
<div class="col-span-5">
|
</div>
|
||||||
<label class="block text-xs font-medium text-gray-700 mb-1">Description</label>
|
<div class="flex items-center space-x-2">
|
||||||
<div data-item="${itemId}" data-field="description"
|
<svg class="item-chevron w-5 h-5 text-gray-500 transition-transform" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
class="item-description-editor border border-gray-300 rounded-md bg-white"
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
|
||||||
style="min-height: 80px;">
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<input type="hidden" data-item="${itemId}" data-field="description-html" class="item-description-html">
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col-span-2">
|
|
||||||
<label class="block text-xs font-medium text-gray-700 mb-1">Rate</label>
|
<!-- Expandable Content (hidden by default) -->
|
||||||
<input type="text" data-item="${itemId}" data-field="rate"
|
<div class="item-content hidden">
|
||||||
value="${item ? item.rate : ''}"
|
<div class="grid grid-cols-12 gap-3 items-start p-4">
|
||||||
class="item-input w-full px-2 py-2 border border-gray-300 rounded-md text-sm focus:ring-blue-500 focus:border-blue-500">
|
<div class="col-span-1">
|
||||||
</div>
|
<label class="block text-xs font-medium text-gray-700 mb-1">Qty</label>
|
||||||
<div class="col-span-2">
|
<input type="text" data-item="${itemId}" data-field="quantity"
|
||||||
<label class="block text-xs font-medium text-gray-700 mb-1">Amount</label>
|
value="${item ? item.quantity : ''}"
|
||||||
<input type="text" data-item="${itemId}" data-field="amount"
|
class="item-input w-full px-2 py-2 border border-gray-300 rounded-md text-sm focus:ring-blue-500 focus:border-blue-500">
|
||||||
value="${item ? item.amount : ''}"
|
</div>
|
||||||
class="item-amount w-full px-2 py-2 border border-gray-300 rounded-md text-sm focus:ring-blue-500 focus:border-blue-500">
|
<div class="col-span-5">
|
||||||
</div>
|
<label class="block text-xs font-medium text-gray-700 mb-1">Description</label>
|
||||||
<div class="col-span-1">
|
<div data-item="${itemId}" data-field="description"
|
||||||
<label class="block text-xs font-medium text-gray-700 mb-1">TBD</label>
|
class="item-description-editor border border-gray-300 rounded-md bg-white"
|
||||||
<input type="checkbox" data-item="${itemId}" data-field="is_tbd"
|
style="min-height: 80px;">
|
||||||
${item && item.is_tbd ? 'checked' : ''}
|
</div>
|
||||||
class="item-tbd h-5 w-5 mt-2 text-blue-600 focus:ring-blue-500 border-gray-300 rounded">
|
<input type="hidden" data-item="${itemId}" data-field="description-html" class="item-description-html">
|
||||||
</div>
|
</div>
|
||||||
<div class="col-span-1 flex items-end">
|
<div class="col-span-2">
|
||||||
<button type="button" onclick="removeQuoteItem(${itemId})"
|
<label class="block text-xs font-medium text-gray-700 mb-1">Rate</label>
|
||||||
class="w-full px-2 py-2 bg-red-500 hover:bg-red-600 text-white rounded-md text-sm">
|
<input type="text" data-item="${itemId}" data-field="rate"
|
||||||
×
|
value="${item ? item.rate : ''}"
|
||||||
</button>
|
class="item-input w-full px-2 py-2 border border-gray-300 rounded-md text-sm focus:ring-blue-500 focus:border-blue-500">
|
||||||
|
</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="item-amount w-full px-2 py-2 border border-gray-300 rounded-md text-sm focus:ring-blue-500 focus:border-blue-500">
|
||||||
|
</div>
|
||||||
|
<div class="col-span-1">
|
||||||
|
<label class="block text-xs font-medium text-gray-700 mb-1">TBD</label>
|
||||||
|
<input type="checkbox" data-item="${itemId}" data-field="is_tbd"
|
||||||
|
${item && item.is_tbd ? 'checked' : ''}
|
||||||
|
class="item-tbd h-5 w-5 mt-2 text-blue-600 focus:ring-blue-500 border-gray-300 rounded">
|
||||||
|
</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>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|
@ -375,6 +399,7 @@ function addQuoteItem(item = null) {
|
||||||
// Save HTML content on change
|
// Save HTML content on change
|
||||||
quill.on('text-change', () => {
|
quill.on('text-change', () => {
|
||||||
hiddenInput.value = quill.root.innerHTML;
|
hiddenInput.value = quill.root.innerHTML;
|
||||||
|
updateItemSummary(itemId);
|
||||||
updateTotals();
|
updateTotals();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -396,12 +421,17 @@ function addQuoteItem(item = null) {
|
||||||
const amount = qty * rateValue;
|
const amount = qty * rateValue;
|
||||||
amountInput.value = amount.toFixed(2);
|
amountInput.value = amount.toFixed(2);
|
||||||
}
|
}
|
||||||
|
updateItemSummary(itemId);
|
||||||
updateTotals();
|
updateTotals();
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add event listeners for auto-calculation
|
// Add event listeners for auto-calculation and summary update
|
||||||
qtyInput.addEventListener('input', calculateAmount);
|
qtyInput.addEventListener('input', calculateAmount);
|
||||||
rateInput.addEventListener('input', calculateAmount);
|
rateInput.addEventListener('input', calculateAmount);
|
||||||
|
amountInput.addEventListener('input', () => {
|
||||||
|
updateItemSummary(itemId);
|
||||||
|
updateTotals();
|
||||||
|
});
|
||||||
|
|
||||||
// Add event listeners for totals update
|
// Add event listeners for totals update
|
||||||
itemDiv.querySelectorAll('.item-input, .item-amount').forEach(input => {
|
itemDiv.querySelectorAll('.item-input, .item-amount').forEach(input => {
|
||||||
|
|
@ -438,6 +468,41 @@ function removeQuoteItem(itemId) {
|
||||||
updateTotals();
|
updateTotals();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toggleItem(itemId) {
|
||||||
|
const itemDiv = document.getElementById(`item-${itemId}`);
|
||||||
|
const content = itemDiv.querySelector('.item-content');
|
||||||
|
const chevron = itemDiv.querySelector('.item-chevron');
|
||||||
|
|
||||||
|
if (content.classList.contains('hidden')) {
|
||||||
|
content.classList.remove('hidden');
|
||||||
|
chevron.style.transform = 'rotate(180deg)';
|
||||||
|
} else {
|
||||||
|
content.classList.add('hidden');
|
||||||
|
chevron.style.transform = 'rotate(0deg)';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateItemSummary(itemId) {
|
||||||
|
const itemDiv = document.getElementById(`item-${itemId}`);
|
||||||
|
const qtyInput = itemDiv.querySelector('[data-field="quantity"]');
|
||||||
|
const amountInput = itemDiv.querySelector('[data-field="amount"]');
|
||||||
|
const descEditor = itemDiv.querySelector('.item-description-editor');
|
||||||
|
|
||||||
|
// Update summary displays
|
||||||
|
const summaryQty = itemDiv.querySelector('.item-summary-qty');
|
||||||
|
const summaryDesc = itemDiv.querySelector('.item-summary-desc');
|
||||||
|
const summaryAmount = itemDiv.querySelector('.item-summary-amount');
|
||||||
|
|
||||||
|
if (summaryQty) summaryQty.textContent = qtyInput.value || '0';
|
||||||
|
if (summaryAmount) summaryAmount.textContent = amountInput.value || '0.00';
|
||||||
|
|
||||||
|
if (summaryDesc && descEditor.quillInstance) {
|
||||||
|
const plainText = descEditor.quillInstance.getText().trim();
|
||||||
|
const preview = plainText.substring(0, 60) + (plainText.length > 60 ? '...' : '');
|
||||||
|
summaryDesc.textContent = preview || 'New item';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function updateTotals() {
|
function updateTotals() {
|
||||||
const items = getQuoteItems();
|
const items = getQuoteItems();
|
||||||
const taxExempt = document.getElementById('quote-tax-exempt').checked;
|
const taxExempt = document.getElementById('quote-tax-exempt').checked;
|
||||||
|
|
@ -474,17 +539,18 @@ function getQuoteItems() {
|
||||||
const itemDivs = document.querySelectorAll('#quote-items > div');
|
const itemDivs = document.querySelectorAll('#quote-items > div');
|
||||||
|
|
||||||
itemDivs.forEach(div => {
|
itemDivs.forEach(div => {
|
||||||
const descEditor = div.querySelector('.item-description-editor');
|
const content = div.querySelector('.item-content');
|
||||||
|
const descEditor = content.querySelector('.item-description-editor');
|
||||||
const descriptionHTML = descEditor && descEditor.quillInstance
|
const descriptionHTML = descEditor && descEditor.quillInstance
|
||||||
? descEditor.quillInstance.root.innerHTML
|
? descEditor.quillInstance.root.innerHTML
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
const item = {
|
const item = {
|
||||||
quantity: div.querySelector('[data-field="quantity"]').value,
|
quantity: content.querySelector('[data-field="quantity"]').value,
|
||||||
description: descriptionHTML,
|
description: descriptionHTML,
|
||||||
rate: div.querySelector('[data-field="rate"]').value,
|
rate: content.querySelector('[data-field="rate"]').value,
|
||||||
amount: div.querySelector('[data-field="amount"]').value,
|
amount: content.querySelector('[data-field="amount"]').value,
|
||||||
is_tbd: div.querySelector('[data-field="is_tbd"]').checked
|
is_tbd: content.querySelector('[data-field="is_tbd"]').checked
|
||||||
};
|
};
|
||||||
items.push(item);
|
items.push(item);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue