// Invoice Form Logic const getInvoiceDiscountInput = (tableBody) => { const form = tableBody && typeof tableBody.closest === 'function' ? tableBody.closest('form') : null; return form ? form.querySelector('[data-invoice-discount-input]') : null; }; const getInvoiceDiscountValue = (tableBody) => { const input = getInvoiceDiscountInput(tableBody); if (!input) return 0; let value = parseFloat(input.value); if (!Number.isFinite(value) || value < 0) value = 0; return value; }; const getInvoiceManualDiscountPercent = () => { let value = (typeof companySettings !== 'undefined' && companySettings && companySettings.manual_discount_profit_limit_percent !== undefined) ? parseFloat(companySettings.manual_discount_profit_limit_percent) : 5; if (!Number.isFinite(value) || value < 0) value = 5; return Math.min(100, Math.max(0, value)); }; const getInvoiceManualDiscountMetrics = (tableBody) => { const percent = getInvoiceManualDiscountPercent(); if (!tableBody || invoiceType !== 'sale') { return { percent, profitAmount: 0, maxDiscount: 0 }; } let profitAmount = 0; tableBody.querySelectorAll('.item-row').forEach(row => { const qty = normalizeQuantity(row.querySelector('.item-qty')?.value || 0); const unitPrice = parseFloat(row.querySelector('.item-price')?.value) || 0; const purchasePrice = parseFloat(row.querySelector('.item-purchase-price')?.value) || 0; profitAmount += (unitPrice - purchasePrice) * qty; }); profitAmount = Math.max(0, profitAmount); return { percent, profitAmount, maxDiscount: Math.max(0, profitAmount * (percent / 100)) }; }; const updateInvoiceDiscountHelp = (tableBody, limitAmount = 0, metrics = null) => { const input = getInvoiceDiscountInput(tableBody); if (!input) return; const form = tableBody && typeof tableBody.closest === 'function' ? tableBody.closest('form') : null; const help = form ? form.querySelector('[data-invoice-discount-help]') : null; if (!help) return; const activeMetrics = metrics || getInvoiceManualDiscountMetrics(tableBody); const safeLimit = Math.max(0, Number(limitAmount) || 0); const percentLabel = activeMetrics.percent.toFixed(3).replace(/\.0+$/, '').replace(/(\.\d*[1-9])0+$/, '$1'); const isArabic = (document.documentElement.lang || 'en') === 'ar'; help.textContent = isArabic ? `الحد الأقصى الآن: ${safeLimit.toFixed(3)} (${percentLabel}% من ربح الفاتورة ${activeMetrics.profitAmount.toFixed(3)})` : `Max allowed now: ${safeLimit.toFixed(3)} (${percentLabel}% of invoice profit ${activeMetrics.profitAmount.toFixed(3)})`; }; 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; const discountInput = getInvoiceDiscountInput(tableBody); if (discountInput && discountInput.dataset.discountBound !== '1') { discountInput.dataset.discountBound = '1'; discountInput.addEventListener('input', function() { const nextValue = parseFloat(this.value); if (Number.isFinite(nextValue) && nextValue < 0) { this.value = '0'; } recalculate(tableBody, grandTotalEl, subtotalEl, totalVatEl); }); } 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 = `
${item.sku} - ${item.name_en} / ${item.name_ar} Stock: ${formatQuantity(item.stock_quantity)}
`; 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; const purchasePrice = customData && customData.purchase_price !== undefined && customData.purchase_price !== null ? customData.purchase_price : (item.purchase_price || 0); row.innerHTML = `
${item.name_en}
${item.name_ar} (${item.sku})
`; 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 rawGrandTotal = subtotal + totalVat; const discountInput = getInvoiceDiscountInput(tableBody); const discountMetrics = getInvoiceManualDiscountMetrics(tableBody); const maxDiscountAmount = discountInput ? Math.min(rawGrandTotal, discountMetrics.maxDiscount) : rawGrandTotal; let discountAmount = getInvoiceDiscountValue(tableBody); if (discountAmount > maxDiscountAmount) { discountAmount = maxDiscountAmount; if (discountInput) discountInput.value = discountAmount.toFixed(3); } updateInvoiceDiscountHelp(tableBody, maxDiscountAmount, discountMetrics); const grandTotal = Math.max(0, rawGrandTotal - discountAmount); 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 = ''; initInvoiceForm('productSearchInput', 'searchSuggestions', 'invoiceItemsTableBody', 'grandTotal', 'subtotal', 'totalVat'); initInvoiceForm('editProductSearchInput', 'editSearchSuggestions', 'editInvoiceItemsTableBody', 'edit_grandTotal', 'edit_subtotal', 'edit_totalVat');