252 lines
12 KiB
PHP
252 lines
12 KiB
PHP
// Invoice Form Logic
|
|
const initInvoiceForm = (searchInputId, suggestionsId, tableBodyId, grandTotalId, subtotalId, totalVatId) => {
|
|
const searchInput = document.getElementById(searchInputId);
|
|
const suggestions = document.getElementById(suggestionsId);
|
|
const tableBody = document.getElementById(tableBodyId);
|
|
const grandTotalEl = document.getElementById(grandTotalId);
|
|
const subtotalEl = document.getElementById(subtotalId);
|
|
const totalVatEl = document.getElementById(totalVatId);
|
|
|
|
if (!searchInput || !tableBody) return;
|
|
|
|
let timeout = null;
|
|
searchInput.addEventListener('input', function() {
|
|
clearTimeout(timeout);
|
|
const q = this.value.trim();
|
|
if (q.length < 2) {
|
|
suggestions.style.display = 'none';
|
|
return;
|
|
}
|
|
|
|
timeout = setTimeout(() => {
|
|
fetch(`index.php?action=search_items&q=${encodeURIComponent(q)}`)
|
|
.then(res => res.ok ? res.json() : [])
|
|
.then(data => {
|
|
suggestions.innerHTML = '';
|
|
if (data.length > 0) {
|
|
data.forEach(item => {
|
|
const btn = document.createElement('button');
|
|
btn.type = 'button';
|
|
btn.className = 'list-group-item list-group-item-action';
|
|
btn.innerHTML = `
|
|
<div class="d-flex justify-content-between">
|
|
<span><strong>${item.sku}</strong> - ${item.name_en} / ${item.name_ar}</span>
|
|
<span class="text-muted small">Stock: ${formatQuantity(item.stock_quantity)}</span>
|
|
</div>
|
|
`;
|
|
btn.onclick = () => addItemToTable(item, tableBody, searchInput, suggestions, grandTotalEl, subtotalEl, totalVatEl);
|
|
suggestions.appendChild(btn);
|
|
});
|
|
suggestions.style.display = 'block';
|
|
} else {
|
|
suggestions.style.display = 'none';
|
|
}
|
|
});
|
|
}, 300);
|
|
});
|
|
|
|
// Close suggestions when clicking outside
|
|
document.addEventListener('click', function(e) {
|
|
if (!searchInput.contains(e.target) && !suggestions.contains(e.target)) {
|
|
suggestions.style.display = 'none';
|
|
}
|
|
});
|
|
};
|
|
|
|
|
|
const invoiceSyncSelect2Value = (select) => {
|
|
if (!select) return;
|
|
if (select.classList.contains('select2') && window.jQuery && window.jQuery.fn && window.jQuery.fn.select2) {
|
|
window.jQuery(select).trigger('change');
|
|
}
|
|
};
|
|
|
|
const invoiceEnsureSelectOption = (selectId, value, label = '') => {
|
|
const select = document.getElementById(selectId);
|
|
if (!select || value === null || value === undefined || String(value) === '') return;
|
|
|
|
const alreadyExists = Array.from(select.options || []).some(option => option.value == String(value));
|
|
if (!alreadyExists) {
|
|
const option = document.createElement('option');
|
|
option.value = String(value);
|
|
option.textContent = label || String(value);
|
|
select.appendChild(option);
|
|
}
|
|
};
|
|
|
|
const invoiceSetBlankSelectOptionLabel = (select, label = '---') => {
|
|
if (!select) return;
|
|
const emptyOption = Array.from(select.options || []).find(option => option.value === '');
|
|
if (emptyOption) {
|
|
emptyOption.textContent = label;
|
|
}
|
|
};
|
|
|
|
const invoiceShowConfirmDialog = async ({
|
|
title = 'Are you sure?',
|
|
text = '',
|
|
confirmButtonText = 'Yes, continue',
|
|
cancelButtonText = 'Cancel',
|
|
icon = 'warning'
|
|
} = {}) => {
|
|
if (window.Swal) {
|
|
const result = await Swal.fire({
|
|
title,
|
|
text,
|
|
icon,
|
|
showCancelButton: true,
|
|
confirmButtonText,
|
|
cancelButtonText,
|
|
reverseButtons: true,
|
|
focusCancel: true,
|
|
confirmButtonColor: '#dc3545',
|
|
cancelButtonColor: '#6c757d'
|
|
});
|
|
return !!result.isConfirmed;
|
|
}
|
|
|
|
return window.confirm(text ? `${title}
|
|
|
|
${text}` : title);
|
|
};
|
|
|
|
const bindInvoiceSweetConfirmForms = () => {
|
|
document.querySelectorAll('.js-swal-confirm-form').forEach(form => {
|
|
if (form.dataset.confirmBound === '1') return;
|
|
form.dataset.confirmBound = '1';
|
|
|
|
form.addEventListener('submit', async function(event) {
|
|
if (form.dataset.skipConfirm === '1') {
|
|
delete form.dataset.skipConfirm;
|
|
return;
|
|
}
|
|
|
|
event.preventDefault();
|
|
|
|
const confirmed = await invoiceShowConfirmDialog({
|
|
title: form.dataset.confirmTitle || 'Are you sure?',
|
|
text: form.dataset.confirmText || '',
|
|
confirmButtonText: form.dataset.confirmButton || 'Yes, continue',
|
|
cancelButtonText: form.dataset.cancelButton || 'Cancel',
|
|
icon: form.dataset.confirmIcon || 'warning'
|
|
});
|
|
|
|
if (!confirmed) return;
|
|
|
|
const submitter = event.submitter || null;
|
|
if (submitter && typeof form.requestSubmit === 'function') {
|
|
form.dataset.skipConfirm = '1';
|
|
form.requestSubmit(submitter);
|
|
return;
|
|
}
|
|
|
|
HTMLFormElement.prototype.submit.call(form);
|
|
});
|
|
});
|
|
};
|
|
|
|
bindInvoiceSweetConfirmForms();
|
|
|
|
function addItemToTable(item, tableBody, searchInput, suggestions, grandTotalEl, subtotalEl, totalVatEl, customData = null) {
|
|
if (suggestions) suggestions.style.display = 'none';
|
|
if (searchInput) searchInput.value = '';
|
|
|
|
const allowZeroStock = (typeof companySettings !== 'undefined' && String(companySettings.allow_zero_stock_sell) === '1');
|
|
const currentStock = normalizeQuantity(item.stock_quantity);
|
|
|
|
if (invoiceType === 'sale' && !allowZeroStock && !customData) {
|
|
const existingInTable = Array.from(tableBody.querySelectorAll('.item-row')).find(row => row.querySelector('.item-id-input').value == item.id);
|
|
let currentQtyInTable = 0;
|
|
if (existingInTable) {
|
|
currentQtyInTable = normalizeQuantity(existingInTable.querySelector('.item-qty').value);
|
|
}
|
|
|
|
if (currentQtyInTable + 1 > currentStock) {
|
|
alert('Insufficient stock! Available: ' + formatQuantity(currentStock));
|
|
return;
|
|
}
|
|
}
|
|
|
|
const existingRow = Array.from(tableBody.querySelectorAll('.item-id-input')).find(input => input.value == item.id);
|
|
if (existingRow && !customData) {
|
|
const row = existingRow.closest('tr');
|
|
const qtyInput = row.querySelector('.item-qty');
|
|
qtyInput.value = formatQuantity((parseFloat(qtyInput.value) || 0) + 1);
|
|
recalculate(tableBody, grandTotalEl, subtotalEl, totalVatEl);
|
|
return;
|
|
}
|
|
|
|
const row = document.createElement('tr');
|
|
row.className = 'item-row';
|
|
const price = customData ? customData.unit_price : (invoiceType === 'sale' ? item.sale_price : item.purchase_price);
|
|
const qty = normalizeQuantity(customData ? customData.quantity : 1);
|
|
const vatRate = item.vat_rate || 0;
|
|
|
|
row.innerHTML = `
|
|
<td>
|
|
<input type="hidden" name="item_ids[]" class="item-id-input" value="${item.id}">
|
|
<input type="hidden" class="item-row-stock" value="${formatQuantity(item.stock_quantity)}">
|
|
<input type="hidden" class="item-vat-rate" value="${vatRate}">
|
|
<div><strong>${item.name_en}</strong></div>
|
|
<div class="small text-muted">${item.name_ar} (${item.sku})</div>
|
|
</td>
|
|
<td><input type="number" step="0.01" name="quantities[]" class="form-control item-qty" value="${formatQuantity(qty)}" required></td>
|
|
<td><input type="number" step="0.001" name="prices[]" class="form-control item-price" value="${price}" required></td>
|
|
<td><input type="text" class="form-control bg-light" value="${parseFloat(vatRate || 0).toFixed(2)}%" readonly></td>
|
|
<td><input type="number" step="0.001" class="form-control item-total" value="${(qty * price).toFixed(3)}" readonly></td>
|
|
<td><button type="button" class="btn btn-outline-danger btn-sm remove-row"><i class="bi bi-trash"></i></button></td>
|
|
`;
|
|
|
|
tableBody.appendChild(row);
|
|
if (typeof syncQuantityInputs === 'function') syncQuantityInputs(row);
|
|
attachRowListeners(row, tableBody, grandTotalEl, subtotalEl, totalVatEl);
|
|
recalculate(tableBody, grandTotalEl, subtotalEl, totalVatEl);
|
|
}
|
|
|
|
function recalculate(tableBody, grandTotalEl, subtotalEl, totalVatEl) {
|
|
let subtotal = 0;
|
|
let totalVat = 0;
|
|
tableBody.querySelectorAll('.item-row').forEach(row => {
|
|
const qtyInput = row.querySelector('.item-qty');
|
|
const qty = normalizeQuantity(qtyInput ? qtyInput.value : 0);
|
|
const price = parseFloat(row.querySelector('.item-price').value) || 0;
|
|
const vatRate = parseFloat(row.querySelector('.item-vat-rate').value) || 0;
|
|
|
|
const total = qty * price;
|
|
const vatAmount = total * (vatRate / 100);
|
|
|
|
row.querySelector('.item-total').value = total.toFixed(3);
|
|
subtotal += total;
|
|
totalVat += vatAmount;
|
|
});
|
|
const grandTotal = subtotal + totalVat;
|
|
|
|
if (subtotalEl) subtotalEl.textContent = 'OMR ' + subtotal.toFixed(3);
|
|
if (totalVatEl) totalVatEl.textContent = 'OMR ' + totalVat.toFixed(2);
|
|
if (grandTotalEl) grandTotalEl.textContent = 'OMR ' + grandTotal.toFixed(3);
|
|
}
|
|
|
|
function attachRowListeners(row, tableBody, grandTotalEl, subtotalEl, totalVatEl) {
|
|
row.querySelector('.item-qty').addEventListener('input', function() {
|
|
const allowZeroStock = (typeof companySettings !== 'undefined' && String(companySettings.allow_zero_stock_sell) === '1');
|
|
if (invoiceType === 'sale' && !allowZeroStock) {
|
|
const stock = normalizeQuantity(row.querySelector('.item-row-stock').value);
|
|
const qty = normalizeQuantity(this.value);
|
|
if (qty > stock) {
|
|
alert('Insufficient stock! Available: ' + formatQuantity(stock));
|
|
this.value = formatQuantity(stock);
|
|
}
|
|
}
|
|
recalculate(tableBody, grandTotalEl, subtotalEl, totalVatEl);
|
|
});
|
|
row.querySelector('.item-price').addEventListener('input', () => recalculate(tableBody, grandTotalEl, subtotalEl, totalVatEl));
|
|
row.querySelector('.remove-row').addEventListener('click', function() {
|
|
row.remove();
|
|
recalculate(tableBody, grandTotalEl, subtotalEl, totalVatEl);
|
|
});
|
|
}
|
|
|
|
const invoiceType = '<?= in_array($page, ["sales", "quotations"]) ? "sale" : (in_array($page, ["purchases", "lpos"]) ? "purchase" : "") ?>';
|
|
initInvoiceForm('productSearchInput', 'searchSuggestions', 'invoiceItemsTableBody', 'grandTotal', 'subtotal', 'totalVat');
|
|
initInvoiceForm('editProductSearchInput', 'editSearchSuggestions', 'editInvoiceItemsTableBody', 'edit_grandTotal', 'edit_subtotal', 'edit_totalVat');
|