diff --git a/assets/pasted-20260508-022307-a1e2a6ee.png b/assets/pasted-20260508-022307-a1e2a6ee.png new file mode 100644 index 0000000..ecf52a0 Binary files /dev/null and b/assets/pasted-20260508-022307-a1e2a6ee.png differ diff --git a/assets/pasted-20260508-024023-b5ab1c09.png b/assets/pasted-20260508-024023-b5ab1c09.png new file mode 100644 index 0000000..8e58e70 Binary files /dev/null and b/assets/pasted-20260508-024023-b5ab1c09.png differ diff --git a/assets/pasted-20260508-025121-d187b3f9.webp b/assets/pasted-20260508-025121-d187b3f9.webp new file mode 100644 index 0000000..668b721 Binary files /dev/null and b/assets/pasted-20260508-025121-d187b3f9.webp differ diff --git a/assets/vm-shot-2026-05-08T02-21-58-361Z.jpg b/assets/vm-shot-2026-05-08T02-21-58-361Z.jpg new file mode 100644 index 0000000..33a1e9d Binary files /dev/null and b/assets/vm-shot-2026-05-08T02-21-58-361Z.jpg differ diff --git a/index.php b/index.php index 3641ccf..e52e92f 100644 --- a/index.php +++ b/index.php @@ -1861,7 +1861,7 @@ if (isset($_GET['action']) || isset($_POST['action'])) { // Render Card HTML ?> -
+
<?= htmlspecialchars($p['name_en']) ?> @@ -2014,9 +2014,23 @@ if (isset($_GET['action']) || isset($_POST['action'])) { $tax_amount = (float)($_POST['tax_amount'] ?? 0); $discount_code_id = !empty($_POST['discount_code_id']) ? (int)$_POST['discount_code_id'] : null; $discount_amount = max(0, (float)($_POST['discount_amount'] ?? 0)); + $manual_discount_amount = max(0, (float)($_POST['manual_discount_amount'] ?? 0)); $manualDiscountEnabled = getSettingValue('manual_discount_enabled', '0') === '1'; - if (!$manualDiscountEnabled && $discount_code_id === null) { - $discount_amount = 0.0; + if (!$manualDiscountEnabled) { + $manual_discount_amount = 0.0; + if ($discount_code_id === null) { + $discount_amount = 0.0; + } + } + if ($manual_discount_amount > ($discount_amount + 0.0005)) { + throw new Exception(($lang ?? 'en') === 'ar' ? 'بيانات الخصم اليدوي غير صالحة.' : 'Invalid manual discount data.'); + } + if ($manual_discount_amount > 0) { + $discountMetrics = calculateManualDiscountProfitMetrics($items, true); + $maxManualDiscount = min(max(0, $total_amount), max(0, (float)($discountMetrics['max_discount'] ?? 0))); + if ($manual_discount_amount > ($maxManualDiscount + 0.0005)) { + throw new Exception(manualDiscountLimitMessage($discountMetrics, $manual_discount_amount)); + } } if ($discount_amount > $total_amount) { $discount_amount = max(0, $total_amount); @@ -2166,7 +2180,7 @@ if (isset($_GET['action']) || isset($_POST['action'])) { $invoice['party_name'] = $partyName !== '' ? $partyName : '---'; $invoice['paid_amount'] = (float)($invoice['paid_amount'] ?? 0); - $stmtItems = db()->prepare("SELECT li.*, i.name_en, i.name_ar, i.sku, i.vat_rate, i.stock_quantity FROM $itemTable li LEFT JOIN stock_items i ON li.item_id = i.id WHERE li.$fkColumn = ?"); + $stmtItems = db()->prepare("SELECT li.*, i.name_en, i.name_ar, i.sku, i.vat_rate, i.stock_quantity, i.purchase_price FROM $itemTable li LEFT JOIN stock_items i ON li.item_id = i.id WHERE li.$fkColumn = ?"); $stmtItems->execute([$invoice_id]); $invoice['items'] = $stmtItems->fetchAll(PDO::FETCH_ASSOC); @@ -2725,6 +2739,113 @@ function getSettingValue(string $key, ?string $default = null): ?string { return $value; } +function getManualDiscountProfitLimitPercent(): float { + $rawValue = getSettingValue('manual_discount_profit_limit_percent', '5'); + $percent = is_numeric($rawValue) ? (float)$rawValue : 5.0; + if ($percent < 0) { + $percent = 0.0; + } + if ($percent > 100) { + $percent = 100.0; + } + return $percent; +} + +function calculateManualDiscountProfitMetrics(array $lines, bool $pricesIncludeVat = false): array { + $percent = getManualDiscountProfitLimitPercent(); + $normalizedLines = []; + $itemIds = []; + + foreach ($lines as $line) { + $itemId = (int)($line['item_id'] ?? $line['id'] ?? 0); + $qty = normalize_quantity($line['qty'] ?? $line['quantity'] ?? 0); + $unitPrice = (float)($line['unit_price'] ?? $line['price'] ?? 0); + $vatRate = isset($line['vat_rate']) ? (float)$line['vat_rate'] : 0.0; + + if ($itemId <= 0 || $qty <= 0) { + continue; + } + + $normalizedLines[] = [ + 'item_id' => $itemId, + 'qty' => $qty, + 'unit_price' => $unitPrice, + 'vat_rate' => $vatRate, + ]; + $itemIds[$itemId] = true; + } + + if ($normalizedLines === [] || $itemIds === []) { + return [ + 'percent' => $percent, + 'profit_amount' => 0.0, + 'max_discount' => 0.0, + ]; + } + + $ids = array_keys($itemIds); + $placeholders = implode(',', array_fill(0, count($ids), '?')); + $purchasePrices = []; + + try { + $stmt = db()->prepare("SELECT id, purchase_price FROM stock_items WHERE id IN ($placeholders)"); + $stmt->execute($ids); + foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) { + $purchasePrices[(int)$row['id']] = (float)($row['purchase_price'] ?? 0); + } + } catch (Throwable $e) { + $purchasePrices = []; + } + + $profitAmount = 0.0; + foreach ($normalizedLines as $line) { + $purchasePrice = $purchasePrices[(int)$line['item_id']] ?? 0.0; + $effectiveUnitPrice = (float)$line['unit_price']; + $vatRate = (float)($line['vat_rate'] ?? 0.0); + + if ($pricesIncludeVat && $vatRate > 0) { + $effectiveUnitPrice = $effectiveUnitPrice / (1 + ($vatRate / 100)); + } + + $profitAmount += ($effectiveUnitPrice - $purchasePrice) * (float)$line['qty']; + } + + $profitAmount = max(0, $profitAmount); + $maxDiscount = max(0, $profitAmount * ($percent / 100)); + + return [ + 'percent' => $percent, + 'profit_amount' => $profitAmount, + 'max_discount' => $maxDiscount, + ]; +} + +function manualDiscountLimitMessage(array $metrics, ?float $requestedAmount = null): string { + $langCode = $_SESSION['lang'] ?? 'en'; + $currency = function_exists('__') ? __('currency') : 'OMR'; + $allowed = number_format(max(0, (float)($metrics['max_discount'] ?? 0)), 3, '.', ''); + $profit = number_format(max(0, (float)($metrics['profit_amount'] ?? 0)), 3, '.', ''); + $percent = rtrim(rtrim(number_format(max(0, (float)($metrics['percent'] ?? 0)), 3, '.', ''), '0'), '.'); + if ($percent === '') { + $percent = '0'; + } + $requested = $requestedAmount !== null ? number_format(max(0, $requestedAmount), 3, '.', '') : null; + + if ($langCode === 'ar') { + $message = "الخصم اليدوي المسموح هو {$currency} {$allowed} فقط ({$percent}% من ربح الفاتورة {$currency} {$profit})."; + if ($requested !== null) { + $message = "الخصم اليدوي {$currency} {$requested} يتجاوز الحد المسموح. " . $message; + } + return $message; + } + + $message = "The maximum allowed manual discount is {$currency} {$allowed} ({$percent}% of invoice profit {$currency} {$profit})."; + if ($requested !== null) { + $message = "Manual discount {$currency} {$requested} exceeds the allowed limit. " . $message; + } + return $message; +} + function getWeightBarcodeConfig(): array { $prefixStart = (int)(getSettingValue('weight_barcode_prefix_start', '20') ?? '20'); $prefixEnd = (int)(getSettingValue('weight_barcode_prefix_end', '29') ?? '29'); @@ -7712,7 +7833,7 @@ runtime_debug_mark('page:rendering', ['page' => (string)$page]);
-
+
<?= htmlspecialchars($p['name_en']) ?> @@ -7799,6 +7920,7 @@ runtime_debug_mark('page:rendering', ['page' => (string)$page]);
+
@@ -7877,6 +7999,47 @@ runtime_debug_mark('page:rendering', ['page' => (string)$page]); isManualDiscountEnabled() { return String((typeof companySettings !== 'undefined' && companySettings && companySettings.manual_discount_enabled !== undefined) ? companySettings.manual_discount_enabled : '0') === '1'; }, + getManualDiscountProfitLimitPercent() { + 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)); + }, + getManualDiscountProfitMetrics() { + const percent = this.getManualDiscountProfitLimitPercent(); + let profitAmount = 0; + + (this.items || []).forEach(item => { + const qty = normalizeQuantity(item.qty); + const grossUnitPrice = parseFloat(item.price) || 0; + const vatRate = (item.vatRate !== undefined && item.vatRate !== null) + ? (parseFloat(item.vatRate) || 0) + : (parseFloat(item.vat_rate) || 0); + const purchasePrice = (item.purchasePrice !== undefined && item.purchasePrice !== null) + ? (parseFloat(item.purchasePrice) || 0) + : (parseFloat(item.purchase_price) || 0); + const netUnitPrice = vatRate > 0 ? (grossUnitPrice / (1 + (vatRate / 100))) : grossUnitPrice; + profitAmount += (netUnitPrice - purchasePrice) * qty; + }); + + profitAmount = Math.max(0, profitAmount); + return { + percent, + profitAmount, + maxDiscount: Math.max(0, profitAmount * (percent / 100)) + }; + }, + updateManualDiscountLimitInfo(limitAmount = null, metrics = null) { + const info = document.getElementById('manualDiscountLimitInfo'); + if (!info || !this.isManualDiscountEnabled()) return; + const activeMetrics = metrics || this.getManualDiscountProfitMetrics(); + const safeLimit = Number.isFinite(limitAmount) ? Math.max(0, limitAmount) : Math.max(0, activeMetrics.maxDiscount); + const percentLabel = activeMetrics.percent.toFixed(3).replace(/\.0+$/, '').replace(/(\.\d*[1-9])0+$/, '$1'); + info.textContent = posIsArabic + ? `الحد الأقصى الآن: ${safeLimit.toFixed(3)} (${percentLabel}% من ربح الفاتورة ${activeMetrics.profitAmount.toFixed(3)})` + : `Max allowed now: ${safeLimit.toFixed(3)} (${percentLabel}% of invoice profit ${activeMetrics.profitAmount.toFixed(3)})`; + }, getCodeDiscountAmount(subtotal) { if (!this.discount) return 0; const rawDiscount = this.discount.type === 'percentage' @@ -7907,7 +8070,10 @@ runtime_debug_mark('page:rendering', ['page' => (string)$page]); return sum + (price * qty * (vatRate / (100 + vatRate))); }, 0); const codeDiscount = this.getCodeDiscountAmount(subtotal); - const manualDiscount = this.getManualDiscountAmount(subtotal - codeDiscount, syncInput); + const profitMetrics = this.getManualDiscountProfitMetrics(); + const manualDiscountLimit = Math.min(Math.max(0, subtotal - codeDiscount), Math.max(0, profitMetrics.maxDiscount)); + const manualDiscount = this.getManualDiscountAmount(manualDiscountLimit, syncInput); + this.updateManualDiscountLimitInfo(manualDiscountLimit, profitMetrics); const discountAmount = Math.min(subtotal, codeDiscount + manualDiscount); const redeemSwitch = document.getElementById('redeemLoyalty'); const redeemRate = (this.loyaltySettings && this.loyaltySettings.redeemPointsPerUnit) ? this.loyaltySettings.redeemPointsPerUnit : 100; @@ -7916,7 +8082,7 @@ runtime_debug_mark('page:rendering', ['page' => (string)$page]); ? Math.min(Math.max(0, subtotal - discountAmount), availableRedeemValue) : 0; const total = Math.max(0, subtotal - discountAmount - loyaltyRedeemed); - return { subtotal, totalVat, codeDiscount, manualDiscount, discountAmount, loyaltyRedeemed, total }; + return { subtotal, totalVat, codeDiscount, manualDiscount, discountAmount, loyaltyRedeemed, total, manualDiscountLimit, profitMetrics }; }, onManualDiscountChange() { const manualDiscountInput = document.getElementById('manualDiscountAmount'); @@ -7928,6 +8094,7 @@ runtime_debug_mark('page:rendering', ['page' => (string)$page]); const discInfo = document.getElementById('appliedDiscountInfo'); if (discInfo) discInfo.style.display = 'none'; } + this.calculateTotals(true); this.render(); }, broadcast() { @@ -7935,7 +8102,8 @@ runtime_debug_mark('page:rendering', ['page' => (string)$page]); // Ensure items is an array if (!Array.isArray(this.items)) this.items = []; - const totals = this.calculateTotals(false); + const shouldSyncManualDiscountInput = !(document.activeElement && document.activeElement.id === 'manualDiscountAmount'); + const totals = this.calculateTotals(shouldSyncManualDiscountInput); const subtotal = totals.subtotal; const totalVat = totals.totalVat; const discountAmount = totals.discountAmount; @@ -7978,7 +8146,10 @@ runtime_debug_mark('page:rendering', ['page' => (string)$page]); const currentStock = normalizeQuantity(product.stock_quantity); const addQty = Math.max(normalizeQuantity((product.qty !== undefined && product.qty !== null) ? product.qty : 1), 0.01); const unitPrice = (product.price !== undefined && product.price !== null) ? (parseFloat(product.price) || 0) : (parseFloat(product.sale_price) || 0); - const normalizedProduct = {...product, price: unitPrice}; + const purchasePrice = (product.purchasePrice !== undefined && product.purchasePrice !== null) + ? (parseFloat(product.purchasePrice) || 0) + : (parseFloat(product.purchase_price) || 0); + const normalizedProduct = {...product, price: unitPrice, purchasePrice}; const existing = this.items.find(item => item.id === product.id); if (existing) { @@ -7988,6 +8159,7 @@ runtime_debug_mark('page:rendering', ['page' => (string)$page]); } existing.qty = normalizeQuantity(existing.qty + addQty); existing.price = unitPrice; + existing.purchasePrice = purchasePrice; } else { if (!allowZeroStock && currentStock < addQty) { Swal.fire(posT('Error', 'خطأ'), posT('Insufficient stock!', 'المخزون غير كافٍ!'), 'error'); @@ -8146,6 +8318,13 @@ runtime_debug_mark('page:rendering', ['page' => (string)$page]); throw new Error(posT('Invalid server response', 'استجابة غير صالحة من الخادم')); } const lang = document.documentElement.lang || 'en'; + const escapeHtml = (value) => String(value ?? '').replace(/[&<>"']/g, (char) => ({ + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''' + }[char] || char)); let html = '
'; if (carts.length === 0) { html += ` @@ -8155,21 +8334,29 @@ runtime_debug_mark('page:rendering', ['page' => (string)$page]);
`; } carts.forEach(c => { + const cartId = Number.parseInt(c.id, 10); + if (!Number.isFinite(cartId) || cartId <= 0) return; + const cartName = escapeHtml(c.cart_name || (lang === 'ar' ? 'طلب غير مسمى' : 'Untitled Cart')); + const customerName = escapeHtml(c.customer_name || (lang === 'ar' ? 'عميل عابر' : 'Walk-in')); + const createdAtDate = c.created_at ? new Date(c.created_at) : null; + const createdAtText = createdAtDate && !Number.isNaN(createdAtDate.getTime()) + ? createdAtDate.toLocaleString() + : (c.created_at || ''); html += ` -
-
-
${c.cart_name}
+
+
+
${cartName}
- ${c.customer_name || (lang === 'ar' ? 'عميل عابر' : 'Walk-in')} + ${customerName} | - ${new Date(c.created_at).toLocaleString()} + ${escapeHtml(createdAtText)}
-
- -
@@ -8184,6 +8371,36 @@ runtime_debug_mark('page:rendering', ['page' => (string)$page]); width: '700px', customClass: { container: 'held-carts-swal' + }, + didOpen: (popup) => { + const triggerHeldCartAction = (action, id) => { + if (!Number.isFinite(id) || id <= 0) return; + if (action === 'delete') { + this.deleteHeld(id); + return; + } + this.resume(id); + }; + + popup.querySelectorAll('.held-cart-entry').forEach((row) => { + row.addEventListener('keydown', (event) => { + if (event.key === 'Enter' || event.key === ' ') { + event.preventDefault(); + const id = Number.parseInt(row.getAttribute('data-held-cart-id') || '', 10); + triggerHeldCartAction('resume', id); + } + }); + }); + + popup.addEventListener('click', (event) => { + const actionTarget = event.target.closest('[data-held-action]'); + if (!actionTarget) return; + event.preventDefault(); + event.stopPropagation(); + const action = actionTarget.getAttribute('data-held-action'); + const id = Number.parseInt(actionTarget.getAttribute('data-held-cart-id') || '', 10); + triggerHeldCartAction(action, id); + }); } }); } catch (err) { @@ -8271,7 +8488,8 @@ runtime_debug_mark('page:rendering', ['page' => (string)$page]); `; }).join(''); - const totals = this.calculateTotals(false); + const shouldSyncManualDiscountInput = !(document.activeElement && document.activeElement.id === 'manualDiscountAmount'); + const totals = this.calculateTotals(shouldSyncManualDiscountInput); const discountAmount = totals.discountAmount; const loyaltyRedeemedValue = totals.loyaltyRedeemed; const total = totals.total; @@ -8514,6 +8732,7 @@ runtime_debug_mark('page:rendering', ['page' => (string)$page]); const subtotal = totals.subtotal; const totalVat = totals.totalVat; const discountAmount = totals.discountAmount; + const manualDiscountAmount = totals.manualDiscount; const loyaltyRedeemed = totals.loyaltyRedeemed; const formData = new FormData(); @@ -8524,6 +8743,7 @@ runtime_debug_mark('page:rendering', ['page' => (string)$page]); formData.append('tax_amount', totalVat); formData.append('discount_code_id', this.discount ? this.discount.id : ''); formData.append('discount_amount', discountAmount); + formData.append('manual_discount_amount', manualDiscountAmount); formData.append('loyalty_redeemed', loyaltyRedeemed); formData.append('items', JSON.stringify(this.items.map(i => { const vr = (i.vatRate !== undefined && i.vatRate !== null) ? i.vatRate : 0; @@ -8722,6 +8942,7 @@ runtime_debug_mark('page:rendering', ['page' => (string)$page]); nameEn: card.dataset.nameEn, nameAr: card.dataset.nameAr, price: parseFloat(card.dataset.price), + purchasePrice: parseFloat(card.dataset.purchasePrice) || 0, sku: card.dataset.sku, stock_quantity: parseFloat(card.dataset.stockQuantity), vatRate: parseFloat(card.dataset.vatRate) || 0 diff --git a/pages/sales_purchases_invoice_actions_script.php b/pages/sales_purchases_invoice_actions_script.php index 1e9ad7e..37f21bd 100644 --- a/pages/sales_purchases_invoice_actions_script.php +++ b/pages/sales_purchases_invoice_actions_script.php @@ -41,10 +41,12 @@ name_ar: item.name_ar || item.item_name_ar || '', sku: item.sku || '', vat_rate: item.vat_rate || 0, + purchase_price: item.purchase_price || 0, stock_quantity: item.stock_quantity || 0 }, tableBody, null, null, grandTotalEl, subtotalEl, totalVatEl, { quantity: item.quantity, - unit_price: item.unit_price + unit_price: item.unit_price, + purchase_price: item.purchase_price || 0 }); }); }; diff --git a/pages/sales_purchases_invoice_form_helpers.php b/pages/sales_purchases_invoice_form_helpers.php index a84869d..32a33fe 100644 --- a/pages/sales_purchases_invoice_form_helpers.php +++ b/pages/sales_purchases_invoice_form_helpers.php @@ -12,6 +12,51 @@ 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); @@ -206,12 +251,16 @@ ${text}` : title); 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})
@@ -247,11 +296,14 @@ ${text}` : title); 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 > rawGrandTotal) { - discountAmount = rawGrandTotal; + 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); diff --git a/pages/sales_purchases_modals.php b/pages/sales_purchases_modals.php index 73df5e9..817171e 100644 --- a/pages/sales_purchases_modals.php +++ b/pages/sales_purchases_modals.php @@ -496,6 +496,7 @@
+
@@ -612,6 +613,7 @@
+
diff --git a/pages/sales_purchases_print_script.php b/pages/sales_purchases_print_script.php index f3abe80..69d6e6a 100644 --- a/pages/sales_purchases_print_script.php +++ b/pages/sales_purchases_print_script.php @@ -1,72 +1,200 @@ + const escapeHtml = (value) => { + const stringValue = value == null ? '' : String(value); + return stringValue.replace(/[&<>"']/g, (character) => ({ + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''' + }[character] || character)); + }; + + const humanizeInvoiceText = (value) => { + const raw = String(value ?? '').trim(); + if (!raw) { + return '---'; + } + + return raw + .replace(/[_-]+/g, ' ') + .replace(/\s+/g, ' ') + .toLowerCase() + .replace(/\b\w/g, (letter) => letter.toUpperCase()); + }; + + const formatInvoiceCurrency = (amount, decimals = 3) => { + const numericAmount = Number.isFinite(parseFloat(amount)) ? parseFloat(amount) : 0; + return `${numericAmount.toFixed(decimals)}`; + }; + + const setInvoiceText = (id, value) => { + const element = document.getElementById(id); + if (element) { + element.textContent = value == null || value === '' ? '---' : String(value); + } + return element; + }; + + const toggleInvoiceField = (containerId, valueId, value) => { + const container = document.getElementById(containerId); + const valueElement = document.getElementById(valueId); + if (!container || !valueElement) { + return; + } + + if (value) { + valueElement.textContent = value; + container.style.display = 'block'; + } else { + valueElement.textContent = ''; + container.style.display = 'none'; + } + }; + + let invoicePrintOriginalParent = null; + let invoicePrintOriginalNextSibling = null; + let invoicePrintRestoreTimer = null; + + const prepareInvoiceForPrint = () => { + const modal = document.getElementById('viewInvoiceModal'); + if (!modal || !document.body) { + return; + } + + if (!invoicePrintOriginalParent) { + invoicePrintOriginalParent = modal.parentNode; + invoicePrintOriginalNextSibling = modal.nextSibling; + } + + if (modal.parentNode !== document.body) { + document.body.appendChild(modal); + } + + document.body.classList.add('printing-invoice'); + }; + + const restoreInvoiceAfterPrint = () => { + const modal = document.getElementById('viewInvoiceModal'); + if (!document.body) { + return; + } + + document.body.classList.remove('printing-invoice'); + + if (!modal || !invoicePrintOriginalParent || modal.parentNode === invoicePrintOriginalParent) { + return; + } + + invoicePrintOriginalParent.insertBefore(modal, invoicePrintOriginalNextSibling); + }; + + window.printInvoiceDocument = function() { + prepareInvoiceForPrint(); + window.print(); + + if (invoicePrintRestoreTimer) { + window.clearTimeout(invoicePrintRestoreTimer); + } + + invoicePrintRestoreTimer = window.setTimeout(() => { + restoreInvoiceAfterPrint(); + }, 1200); + }; + + window.addEventListener('beforeprint', prepareInvoiceForPrint); + window.addEventListener('afterprint', restoreInvoiceAfterPrint); + window.viewAndPrintA4Invoice = function(data, autoPrint = true) { if (!data) return; - // Reuse view logic - const invoiceDisplayNo = data.document_no || data.transaction_no || ((data.type === 'purchase' ? 'PUR-' : 'INV-') + data.id.toString().padStart(5, '0')); - document.getElementById('invNumber').textContent = invoiceDisplayNo; - document.getElementById('invDate').textContent = data.invoice_date; - document.getElementById('invPaymentType').textContent = data.payment_type ? data.payment_type.toUpperCase() : 'CASH'; - document.getElementById('invCustomerName').textContent = data.customer_name || '---'; - - const phoneEl = document.getElementById('invCustomerPhone'); - const phoneContainer = document.getElementById('invCustomerPhoneContainer'); - if (data.customer_phone) { - phoneEl.textContent = data.customer_phone; - phoneContainer.style.display = 'block'; - } else { - phoneContainer.style.display = 'none'; + + const invoiceType = String(data.type || 'sale').toLowerCase() === 'purchase' ? 'purchase' : 'sale'; + const invoiceDisplayNo = data.document_no || data.transaction_no || ((invoiceType === 'purchase' ? 'PUR-' : 'INV-') + data.id.toString().padStart(5, '0')); + const paymentText = humanizeInvoiceText(data.payment_type || 'cash'); + const statusKey = String(data.status || '').toLowerCase(); + const statusText = statusKey === 'partially_paid' ? 'Partially Paid' : humanizeInvoiceText(statusKey); + const typeText = invoiceType === 'purchase' ? 'Purchase Invoice' : 'Sales Invoice'; + const documentTitle = invoiceType === 'purchase' ? 'Purchase Invoice / فاتورة شراء' : 'Tax Invoice / فاتورة ضريبية'; + const documentSubtitle = invoiceType === 'purchase' ? 'Official purchase record / مستند شراء رسمي' : 'Official tax document / مستند ضريبي رسمي'; + const partyLabelText = invoiceType === 'purchase' ? 'Supplier Details / بيانات المورد' : 'Bill To / بيانات العميل'; + const partyLabelEn = invoiceType === 'purchase' ? 'Supplier Details' : 'Bill To'; + const partyLabelAr = invoiceType === 'purchase' ? 'بيانات المورد' : 'بيانات العميل'; + const formatQty = typeof window.formatQuantity === 'function' + ? window.formatQuantity + : (value) => { + const numericValue = Number.isFinite(parseFloat(value)) ? parseFloat(value) : 0; + return Number.isInteger(numericValue) + ? String(numericValue) + : numericValue.toFixed(3).replace(/\.?0+$/, ''); + }; + + setInvoiceText('invNumber', invoiceDisplayNo); + setInvoiceText('invDate', data.invoice_date || '---'); + setInvoiceText('invPaymentType', paymentText); + setInvoiceText('invPaymentTypeSummary', paymentText); + setInvoiceText('invCustomerName', data.customer_name || '---'); + setInvoiceText('invStatusText', statusText); + setInvoiceText('invDocumentTitle', documentTitle); + setInvoiceText('invDocumentSubtitle', documentSubtitle); + setInvoiceText('invAmountInWords', data.total_in_words || '---'); + + toggleInvoiceField('invCustomerPhoneContainer', 'invCustomerPhone', data.customer_phone || ''); + toggleInvoiceField('invCustomerTaxIdContainer', 'invCustomerTaxId', data.customer_tax_id || ''); + toggleInvoiceField('invOutletRow', 'invOutletName', data.outlet_name || ''); + + const partyLabel = document.getElementById('invPartyLabel'); + if (partyLabel) { + partyLabel.textContent = partyLabelText; + partyLabel.setAttribute('data-en', partyLabelEn); + partyLabel.setAttribute('data-ar', partyLabelAr); } - const taxIdEl = document.getElementById('invCustomerTaxId'); - const taxIdContainer = document.getElementById('invCustomerTaxIdContainer'); - if (data.customer_tax_id) { - taxIdEl.textContent = data.customer_tax_id; - taxIdContainer.style.display = 'block'; - } else { - taxIdContainer.style.display = 'none'; + const invoiceTypeLabel = document.getElementById('invoiceTypeLabel'); + if (invoiceTypeLabel) { + invoiceTypeLabel.textContent = typeText; + invoiceTypeLabel.className = 'invoice-pill ' + (invoiceType === 'purchase' ? 'invoice-pill--purchase' : 'invoice-pill--sale'); } - document.getElementById('invAmountInWords').textContent = data.total_in_words || ''; - - const invOutletEl = document.getElementById('invOutletName'); - if (invOutletEl) { - invOutletEl.textContent = data.outlet_name ? (data.outlet_name) : ''; - invOutletEl.style.display = data.outlet_name ? 'block' : 'none'; - } - - document.getElementById('invPartyLabel').textContent = data.type === 'sale' ? 'Bill To / فاتورة إلى' : 'Bill From / فاتورة من'; - document.getElementById('invPartyLabel').setAttribute('data-en', data.type === 'sale' ? 'Bill To' : 'Bill From'); - document.getElementById('invPartyLabel').setAttribute('data-ar', data.type === 'sale' ? 'فاتورة إلى' : 'فاتورة من'); - document.getElementById('invoiceTypeLabel').textContent = data.type; - document.getElementById('invoiceTypeLabel').className = 'badge text-uppercase ' + (data.type === 'sale' ? 'bg-success' : 'bg-warning'); - const statusLabel = document.getElementById('invoiceStatusLabel'); - let statusClass = 'bg-secondary'; - let statusEn = data.status ? (data.status.charAt(0).toUpperCase() + data.status.slice(1)) : '---'; - if (data.status === 'paid') statusClass = 'bg-success'; - else if (data.status === 'unpaid') statusClass = 'bg-danger'; - else if (data.status === 'partially_paid') { - statusClass = 'bg-warning text-dark'; - statusEn = 'Partially Paid'; + let statusClass = 'invoice-pill--neutral'; + if (statusKey === 'paid') { + statusClass = 'invoice-pill--paid'; + } else if (statusKey === 'unpaid') { + statusClass = 'invoice-pill--unpaid'; + } else if (statusKey === 'partially_paid') { + statusClass = 'invoice-pill--partial'; } - - statusLabel.textContent = statusEn; - statusLabel.className = 'badge text-uppercase ' + statusClass; - + if (statusLabel) { + statusLabel.textContent = statusText; + statusLabel.className = 'invoice-pill ' + statusClass; + } + const body = document.getElementById('invItemsBody'); - body.innerHTML = ''; - if (data.items) { - data.items.forEach(item => { - const tr = document.createElement('tr'); - tr.innerHTML = ` - ${item.name_en} / ${item.name_ar} - ${formatQuantity(item.quantity)} - ${parseFloat(item.unit_price).toFixed(3)} - ${parseFloat(item.vat_rate || 0).toFixed(2)}% - ${parseFloat(item.total_price).toFixed(3)} - `; - body.appendChild(tr); - }); + if (body) { + body.innerHTML = ''; + if (Array.isArray(data.items) && data.items.length > 0) { + data.items.forEach((item, index) => { + const tr = document.createElement('tr'); + const englishName = escapeHtml(item.name_en || item.name || 'Item'); + const arabicName = escapeHtml(item.name_ar || ''); + const itemNameHtml = arabicName + ? `
${englishName}
${arabicName}
` + : `
${englishName}
`; + + tr.innerHTML = ` + ${index + 1} + ${itemNameHtml} + ${formatQty(item.quantity)} + ${formatInvoiceCurrency(item.unit_price, 3)} + ${(Number.isFinite(parseFloat(item.vat_rate)) ? parseFloat(item.vat_rate) : 0).toFixed(2)}% + ${formatInvoiceCurrency(item.total_price, 3)} + `; + body.appendChild(tr); + }); + } else { + body.innerHTML = 'No invoice items / لا توجد أصناف'; + } } + const vatVal = Number.isFinite(parseFloat(data.vat_amount)) ? parseFloat(data.vat_amount) : 0; const totalVal = Number.isFinite(parseFloat(data.total_amount)) ? parseFloat(data.total_amount) : 0; const discountVal = Math.max(0, Number.isFinite(parseFloat(data.discount_amount)) ? parseFloat(data.discount_amount) : 0); @@ -75,63 +203,88 @@ const grandTotalParsed = parseFloat(data.total_with_vat); const grandTotalValue = Number.isFinite(grandTotalParsed) ? grandTotalParsed : Math.max(0, grossBeforeDiscount - discountVal); const subtotalExVat = isPosInvoice ? Math.max(0, grossBeforeDiscount - vatVal) : totalVal; + const paidAmount = Number.isFinite(parseFloat(data.paid_amount)) ? parseFloat(data.paid_amount) : 0; + const balance = Math.max(0, grandTotalValue - paidAmount); - if (document.getElementById('invSubtotal')) document.getElementById('invSubtotal').innerHTML = ' ' + subtotalExVat.toFixed(3); - if (document.getElementById('invVatAmount')) document.getElementById('invVatAmount').innerHTML = ' ' + vatVal.toFixed(2); + const subtotalEl = document.getElementById('invSubtotal'); + if (subtotalEl) subtotalEl.innerHTML = formatInvoiceCurrency(subtotalExVat, 3); + + const vatEl = document.getElementById('invVatAmount'); + if (vatEl) vatEl.innerHTML = formatInvoiceCurrency(vatVal, 3); const discountRow = document.getElementById('invDiscountRow'); const discountAmountEl = document.getElementById('invDiscountAmount'); if (discountRow && discountAmountEl) { if (discountVal > 0) { discountRow.style.display = 'flex'; - discountAmountEl.innerHTML = ' ' + discountVal.toFixed(3); + discountAmountEl.innerHTML = formatInvoiceCurrency(discountVal, 3); } else { discountRow.style.display = 'none'; discountAmountEl.innerHTML = ''; } } - if (document.getElementById('invGrandTotal')) document.getElementById('invGrandTotal').innerHTML = ' ' + grandTotalValue.toFixed(3); + const grandTotalEl = document.getElementById('invGrandTotal'); + if (grandTotalEl) grandTotalEl.innerHTML = formatInvoiceCurrency(grandTotalValue, 3); - if (document.getElementById('invPaidInfo')) document.getElementById('invPaidInfo').innerHTML = ' ' + parseFloat(data.paid_amount || 0).toFixed(3); - const balance = Math.max(0, grandTotalValue - parseFloat(data.paid_amount || 0)); - if (document.getElementById('invBalanceInfo')) document.getElementById('invBalanceInfo').innerHTML = ' ' + balance.toFixed(3); + const paidInfoEl = document.getElementById('invPaidInfo'); + if (paidInfoEl) paidInfoEl.innerHTML = formatInvoiceCurrency(paidAmount, 3); + + const balanceInfoEl = document.getElementById('invBalanceInfo'); + if (balanceInfoEl) balanceInfoEl.innerHTML = formatInvoiceCurrency(balance, 3); - // Generate QR Code for Zakat, Tax and Customs Authority (ZATCA) style or simple formal const companyName = ; const vatNo = ; const qrData = `Seller: ${companyName}\nVAT: ${vatNo}\nInvoice: ${invoiceDisplayNo}\nDate: ${data.invoice_date}\nTotal: ${grandTotalValue.toFixed(3)}`; - const qrUrl = `https://api.qrserver.com/v1/create-qr-code/?size=100x100&data=${encodeURIComponent(qrData)}`; - if (document.getElementById('invQrCode')) { - document.getElementById('invQrCode').innerHTML = `QR Code`; - } - - const viewModal = bootstrap.Modal.getOrCreateInstance(document.getElementById('viewInvoiceModal')); - viewModal.show(); - - if (autoPrint) { - setTimeout(() => { window.print(); }, 1000); + const qrUrl = `https://api.qrserver.com/v1/create-qr-code/?size=112x112&data=${encodeURIComponent(qrData)}`; + const qrCode = document.getElementById('invQrCode'); + if (qrCode) { + qrCode.innerHTML = `QR Code`; } - fetch(`index.php?action=get_payments&invoice_id=${data.id}`) - .then(res => res.json()) - .then(payments => { + const viewModal = bootstrap.Modal.getOrCreateInstance(document.getElementById('viewInvoiceModal')); + viewModal.show(); + + const paymentsPromise = fetch(`index.php?action=get_payments&invoice_id=${data.id}`) + .then((res) => res.json()) + .then((payments) => { const paymentsBody = document.getElementById('invPaymentsBody'); const paymentsSection = document.getElementById('invPaymentsSection'); if (paymentsBody) paymentsBody.innerHTML = ''; - if (payments && payments.length > 0) { + + if (Array.isArray(payments) && payments.length > 0) { if (paymentsBody) { - payments.forEach(p => { - const tr = document.createElement('tr'); - tr.innerHTML = `${p.payment_date}${p.payment_method}OMR ${parseFloat(p.amount).toFixed(3)}`; - paymentsBody.appendChild(tr); + payments.forEach((payment) => { + const paymentChip = document.createElement('div'); + paymentChip.className = 'invoice-payment-pill'; + paymentChip.innerHTML = ` + ${escapeHtml(payment.payment_date || '')} + + ${escapeHtml(humanizeInvoiceText(payment.payment_method || ''))} + + ${formatInvoiceCurrency(payment.amount, 3)} + `; + paymentsBody.appendChild(paymentChip); }); } if (paymentsSection) paymentsSection.style.display = 'block'; - } else { - if (paymentsSection) paymentsSection.style.display = 'none'; + } else if (paymentsSection) { + paymentsSection.style.display = 'none'; } - }).catch(err => console.error('Error fetching payments:', err)); + }) + .catch((err) => { + console.error('Error fetching payments:', err); + const paymentsSection = document.getElementById('invPaymentsSection'); + if (paymentsSection) paymentsSection.style.display = 'none'; + }); + + if (autoPrint) { + paymentsPromise.finally(() => { + setTimeout(() => { + window.printInvoiceDocument(); + }, 450); + }); + } }; @media print { - .no-print, .sidebar, .topbar, .btn, .modal-header, .modal-footer, .d-print-none, - .modal-backdrop { display: none !important; } - body { background: white !important; margin: 0 !important; padding: 0 !important; overflow: visible !important; } - .main-content { margin: 0 !important; padding: 0 !important; background: white !important; } - - /* Hide all modals by default */ - .modal { display: none !important; } - - /* Show ONLY the active modal */ - .modal.show { - position: absolute !important; - left: 0 !important; - top: 0 !important; - margin: 0 !important; - padding: 0 !important; - overflow: visible !important; - display: block !important; - visibility: visible !important; - background: white !important; - width: 100% !important; + .no-print, + .sidebar, + .topbar, + .btn, + .modal-header, + .modal-footer, + .d-print-none, + .modal-backdrop { + display: none !important; } - - .modal.show .modal-dialog { max-width: 100% !important; width: 100% !important; margin: 0 !important; padding: 0 !important; } - .modal.show .modal-content { border: none !important; box-shadow: none !important; background: white !important; } - .modal.show .modal-body { padding: 0 !important; margin: 0 !important; background: white !important; } - + + html, + body { + background: white !important; + margin: 0 !important; + padding: 0 !important; + overflow: visible !important; + width: auto !important; + min-height: auto !important; + height: auto !important; + } + + body.modal-open:not(.printing-receipt) { + visibility: hidden !important; + overflow: visible !important; + padding-right: 0 !important; + } + + body.printing-invoice { + visibility: visible !important; + overflow: visible !important; + padding-right: 0 !important; + } + + body.printing-invoice > :not(#viewInvoiceModal):not(script):not(style) { + display: none !important; + } + + .main-content { + margin: 0 !important; + padding: 0 !important; + background: white !important; + } + + .modal { + display: none !important; + } + + .modal.show, + body.printing-invoice #viewInvoiceModal.modal.show { + position: static !important; + inset: auto !important; + margin: 0 !important; + padding: 0 !important; + overflow: visible !important; + display: block !important; + visibility: visible !important; + background: white !important; + width: auto !important; + } + + .modal.show .modal-dialog { + max-width: 100% !important; + width: 100% !important; + margin: 0 !important; + padding: 0 !important; + } + + .modal.show .modal-content { + border: none !important; + box-shadow: none !important; + background: white !important; + } + + .modal.show .modal-body { + padding: 0 !important; + margin: 0 !important; + background: white !important; + } + + .modal.show * { + visibility: visible !important; + } + @page { size: A4 portrait; - margin: 5mm; + margin: 7mm; } - + + .invoice-print-shell { + padding: 0 !important; + background: white !important; + } + + .invoice-paper, + .invoice-paper * { + -webkit-print-color-adjust: exact !important; + print-color-adjust: exact !important; + } + + .invoice-paper { + border: none !important; + border-radius: 0 !important; + box-shadow: none !important; + } + + .invoice-paper-body { + padding: 12px 12px 8px !important; + } + + .invoice-header { + padding-bottom: 12px !important; + } + + .invoice-paper-body .g-4, + .invoice-paper-body .g-3 { + --bs-gutter-y: 0.75rem !important; + } + + .invoice-paper-body .g-4 { + --bs-gutter-x: 1rem !important; + } + + .invoice-paper-body .g-3 { + --bs-gutter-x: 0.85rem !important; + } + + .invoice-paper-body .mt-4 { + margin-top: 0.95rem !important; + } + + .invoice-paper-body .mt-3 { + margin-top: 0.65rem !important; + } + + .invoice-paper-body .mb-3 { + margin-bottom: 0.65rem !important; + } + + .invoice-paper-body .mb-2 { + margin-bottom: 0.4rem !important; + } + + .invoice-top-accent { + height: 5px !important; + } + + .invoice-logo { + max-height: 58px !important; + } + + .invoice-title { + font-size: 1.1rem !important; + letter-spacing: 0.06em !important; + } + + .invoice-meta-grid { + gap: 0.6rem !important; + margin-top: 0.75rem !important; + } + + .invoice-meta-card { + min-height: 72px !important; + padding: 0.7rem 0.85rem !important; + } + + .invoice-meta-value { + font-size: 0.92rem !important; + } + + .invoice-party-meta { + gap: 0.2rem 0.75rem !important; + margin-top: 0.25rem !important; + } + + .invoice-inline-list { + gap: 0.3rem 0.7rem !important; + } + + .invoice-inline-kv { + gap: 0.12rem !important; + min-width: 92px !important; + } + + .invoice-inline-kv-label { + font-size: 0.54rem !important; + letter-spacing: 0.08em !important; + } + + .invoice-inline-kv strong { + font-size: 0.8rem !important; + line-height: 1.2 !important; + } + + .invoice-note-card--compact { + gap: 0.55rem !important; + } + + .invoice-note-row { + gap: 0.28rem !important; + } + + .invoice-note-value { + font-size: 0.78rem !important; + line-height: 1.35 !important; + } + + .invoice-note-text { + font-size: 0.72rem !important; + line-height: 1.38 !important; + } + + .invoice-summary-grid { + gap: 0.4rem !important; + grid-template-columns: repeat(3, minmax(0, 1fr)) !important; + } + + .invoice-summary-metric { + padding: 0.5rem 0.55rem !important; + border-radius: 12px !important; + gap: 0.22rem !important; + } + + .invoice-summary-metric-label { + font-size: 0.54rem !important; + letter-spacing: 0.05em !important; + } + + .invoice-summary-metric-value { + font-size: 0.84rem !important; + } + + .invoice-summary-metric--total .invoice-summary-metric-value { + font-size: 1rem !important; + } + + .invoice-payment-compact { + margin-top: 0.65rem !important; + padding-top: 0.55rem !important; + } + + .invoice-payment-list { + gap: 0.32rem !important; + } + + .invoice-payment-pill { + padding: 0.28rem 0.5rem !important; + font-size: 0.68rem !important; + } + + .invoice-payment-pill strong { + font-size: 0.72rem !important; + } + + .invoice-section-card, + .invoice-note-card, + .invoice-summary-card, + .invoice-payment-card { + padding: 0.8rem 0.9rem !important; + border-radius: 14px !important; + } + + .invoice-table-wrap, + .invoice-section-card, + .invoice-note-card, + .invoice-summary-card, + .invoice-payment-card, + .invoice-meta-card, + .invoice-qr-card { + box-shadow: none !important; + } + .invoice-printable-container { - padding: 10px !important; + padding: 0 !important; } - - .mt-4 { margin-top: 1rem !important; } - .mt-5 { margin-top: 1.5rem !important; } - .mb-4 { margin-bottom: 1rem !important; } - .p-5 { padding: 1.5rem !important; } - - .table-bordered th, .table-bordered td { border: 1px solid #dee2e6 !important; } - .bg-light { background-color: #f8f9fa !important; -webkit-print-color-adjust: exact; } - .text-primary { color: #0d6efd !important; -webkit-print-color-adjust: exact; } - .badge { border: 1px solid #000; color: #000 !important; } - - /* Ensure the modal is the only thing visible ONLY when a modal is open */ - body.modal-open:not(.printing-receipt) { visibility: hidden !important; } - body.modal-open:not(.printing-receipt) .modal.show { - visibility: visible !important; - display: block !important; - position: absolute !important; - left: 0 !important; - top: 0 !important; - width: 100% !important; + + .table-formal thead th { + background: #0f172a !important; + color: #ffffff !important; + border-color: #0f172a !important; } - body.modal-open:not(.printing-receipt) .modal.show * { visibility: visible !important; } - - /* Old rules that caused blank pages for nested modals */ - /* body.modal-open:not(.printing-receipt) > *:not(.modal):not(.swal2-container) { display: none !important; } */ - /* body.modal-open:not(.printing-receipt) .main-content { display: none !important; } */ - - /* POS Receipt printing specific */ - body.printing-receipt .modal { display: none !important; } - body.printing-receipt .modal-backdrop { display: none !important; } - body.printing-receipt #posPrintArea { - display: flex !important; + + .table-formal tbody tr:nth-child(even) { + background: #f8fafc !important; + } + + .table-formal thead th { + padding: 0.6rem 0.55rem !important; + font-size: 0.65rem !important; + } + + .table-formal tbody td { + padding: 0.55rem 0.55rem !important; + font-size: 0.82rem !important; + } + + .invoice-summary-row { + padding: 0.35rem 0 !important; + font-size: 0.84rem !important; + } + + .invoice-summary-row--total { + padding-top: 0.7rem !important; + font-size: 0.94rem !important; + } + + .invoice-summary-row--total span:last-child { + font-size: 1.18rem !important; + } + + .invoice-footer { + margin-top: 0.95rem !important; + padding-top: 0.8rem !important; + } + + .invoice-signature-line { + width: 150px !important; + padding-top: 0.4rem !important; + } + + .invoice-qr-card { + min-width: 112px !important; + min-height: 112px !important; + padding: 0.55rem !important; + border-radius: 14px !important; + } + + .invoice-qr-card img { + width: 88px !important; + height: 88px !important; + } + + .invoice-footer-note { + font-size: 0.72rem !important; + } + + .invoice-pill, + .invoice-meta-card, + .invoice-summary-card, + .invoice-payment-card, + .invoice-note-card, + .invoice-section-card { + background-clip: padding-box !important; + } + + .table-formal tr, + .invoice-section-card, + .invoice-summary-card, + .invoice-note-card, + .invoice-payment-card, + .invoice-footer { + break-inside: avoid; + page-break-inside: avoid; + } + + body.printing-receipt .modal, + body.printing-receipt .modal-backdrop { + display: none !important; + } + + body.printing-receipt #posPrintArea { + display: flex !important; visibility: visible !important; justify-content: center !important; align-items: flex-start !important; @@ -77,9 +373,11 @@ z-index: 9999 !important; background: white !important; } + body.printing-receipt #posPrintArea * { visibility: visible !important; } + body.printing-receipt #posPrintArea .thermal-receipt-print { position: static !important; left: auto !important; @@ -87,163 +385,769 @@ margin: 0 auto !important; } } - .invoice-logo { max-height: 80px; width: auto; } - .invoice-header { border-bottom: 2px solid #333; padding-bottom: 20px; } - .invoice-title { font-size: 2.5rem; color: #333; letter-spacing: 2px; } - .invoice-info-card { background: #f8f9fa; border-radius: 8px; padding: 15px; height: 100%; } - .table-formal thead th { background: #333; color: #fff; border: none; text-transform: uppercase; font-size: 0.85rem; } + + #viewInvoiceModal .modal-dialog.invoice-print-dialog { + max-width: 980px; + } + + #viewInvoiceModal .modal-content { + background: transparent; + border: none; + } + + .invoice-print-shell { + padding: 1.5rem; + background: linear-gradient(135deg, #eef4ff 0%, #f8fafc 48%, #ffffff 100%); + } + + .invoice-paper { + background: #ffffff; + border: 1px solid #dbe5f0; + border-radius: 26px; + overflow: hidden; + box-shadow: 0 24px 60px rgba(15, 23, 42, 0.12); + } + + .invoice-top-accent { + height: 8px; + background: linear-gradient(90deg, #0f172a 0%, #1d4ed8 55%, #0ea5e9 100%); + } + + .invoice-paper-body { + padding: 2rem; + color: #0f172a; + font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif; + } + + .invoice-header { + border-bottom: 1px solid #dbe5f0; + padding-bottom: 1.5rem; + } + + .invoice-logo { + max-height: 72px; + width: auto; + } + + .invoice-brand-name { + font-size: 1.45rem; + font-weight: 700; + letter-spacing: -0.02em; + color: #0f172a; + line-height: 1.2; + } + + .invoice-contact-line { + margin: 0; + font-size: 0.88rem; + line-height: 1.6; + color: #475569; + } + + .invoice-outlet { + margin-top: 0.6rem; + font-size: 0.76rem; + letter-spacing: 0.16em; + text-transform: uppercase; + font-weight: 700; + color: #1d4ed8; + } + + .invoice-eyebrow { + margin: 0; + font-size: 0.68rem; + letter-spacing: 0.26em; + text-transform: uppercase; + font-weight: 700; + color: #64748b; + } + + .invoice-title { + font-size: 1.38rem; + line-height: 1.25; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; + color: #0f172a; + } + + .invoice-pill-group { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + justify-content: flex-start; + } + + .invoice-pill { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0.42rem 0.85rem; + border-radius: 999px; + border: 1px solid transparent; + font-size: 0.72rem; + font-weight: 700; + letter-spacing: 0.12em; + text-transform: uppercase; + white-space: nowrap; + } + + .invoice-pill--sale { + background: #e0f2fe; + color: #0c4a6e; + border-color: #bae6fd; + } + + .invoice-pill--purchase { + background: #fef3c7; + color: #92400e; + border-color: #fde68a; + } + + .invoice-pill--paid { + background: #dcfce7; + color: #166534; + border-color: #bbf7d0; + } + + .invoice-pill--unpaid { + background: #fee2e2; + color: #991b1b; + border-color: #fecaca; + } + + .invoice-pill--partial { + background: #fef3c7; + color: #9a3412; + border-color: #fcd34d; + } + + .invoice-pill--neutral { + background: #e2e8f0; + color: #334155; + border-color: #cbd5e1; + } + + .invoice-meta-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 0.75rem; + margin-top: 1rem; + } + + .invoice-meta-card { + min-height: 84px; + padding: 0.9rem 1rem; + border: 1px solid #dbe5f0; + border-radius: 16px; + background: #f8fafc; + } + + .invoice-meta-label { + display: block; + margin-bottom: 0.35rem; + font-size: 0.68rem; + font-weight: 700; + letter-spacing: 0.14em; + text-transform: uppercase; + color: #64748b; + } + + .invoice-meta-value { + display: block; + font-size: 1rem; + font-weight: 700; + color: #0f172a; + word-break: break-word; + } + + .invoice-party-meta { + display: flex; + flex-wrap: wrap; + gap: 0.35rem 1rem; + margin-top: 0.3rem; + } + + .invoice-inline-list { + display: flex; + flex-wrap: wrap; + gap: 0.45rem 1rem; + align-items: flex-start; + } + + .invoice-inline-kv { + display: flex; + flex-direction: column; + gap: 0.2rem; + min-width: 118px; + } + + .invoice-inline-kv-label { + font-size: 0.64rem; + font-weight: 700; + letter-spacing: 0.12em; + text-transform: uppercase; + color: #64748b; + line-height: 1.3; + } + + .invoice-inline-kv strong { + font-size: 0.94rem; + font-weight: 700; + color: #0f172a; + line-height: 1.3; + word-break: break-word; + } + + .invoice-section-card, + .invoice-note-card, + .invoice-summary-card, + .invoice-payment-card { + border: 1px solid #dbe5f0; + border-radius: 18px; + padding: 1rem 1.15rem; + } + + .invoice-section-card { + background: #ffffff; + height: 100%; + } + + .invoice-note-card, + .invoice-payment-card { + background: #f8fafc; + } + + .invoice-summary-card { + background: linear-gradient(180deg, #f8fafc 0%, #f1f5f9 100%); + } + + .invoice-section-title { + margin: 0; + font-size: 0.72rem; + font-weight: 700; + letter-spacing: 0.18em; + text-transform: uppercase; + color: #64748b; + } + + .invoice-party-name { + font-size: 1.12rem; + font-weight: 700; + color: #0f172a; + margin: 0.15rem 0 0.4rem; + } + + .invoice-detail-text { + margin: 0; + font-size: 0.9rem; + line-height: 1.55; + color: #475569; + } + + .invoice-detail-text strong { + color: #0f172a; + } + + .invoice-table-wrap { + margin-top: 1.25rem; + border: 1px solid #dbe5f0; + border-radius: 18px; + overflow: hidden; + } + + .table-formal { + margin-bottom: 0; + } + + .table-formal thead th { + background: #0f172a; + color: #ffffff; + border-color: #0f172a; + padding: 0.88rem 0.75rem; + font-size: 0.72rem; + font-weight: 700; + letter-spacing: 0.12em; + text-transform: uppercase; + vertical-align: middle; + } + + .table-formal tbody td { + padding: 0.85rem 0.75rem; + border-color: #e2e8f0; + vertical-align: top; + font-size: 0.9rem; + color: #1e293b; + } + + .table-formal tbody tr:nth-child(even) { + background: #f8fafc; + } + + .invoice-line-no { + width: 56px; + font-weight: 700; + color: #64748b; + } + + .invoice-item-name { + font-weight: 700; + color: #0f172a; + } + + .invoice-item-secondary { + margin-top: 0.2rem; + font-size: 0.78rem; + color: #64748b; + } + + .invoice-amount { + white-space: nowrap; + font-weight: 700; + color: #0f172a; + } + + .invoice-note-card--compact { + display: flex; + flex-direction: column; + gap: 0.85rem; + } + + .invoice-note-row { + display: flex; + flex-direction: column; + gap: 0.35rem; + } + + .invoice-note-divider { + height: 1px; + background: #dbe5f0; + } + + .invoice-note-value { + margin: 0; + font-size: 0.88rem; + line-height: 1.5; + color: #0f172a; + } + + .invoice-note-text { + margin: 0; + font-size: 0.82rem; + line-height: 1.55; + color: #64748b; + } + + .invoice-summary-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 0.75rem; + } + + .invoice-summary-metric { + display: flex; + flex-direction: column; + gap: 0.35rem; + padding: 0.78rem 0.9rem; + border: 1px solid #dbe5f0; + border-radius: 16px; + background: rgba(255, 255, 255, 0.94); + min-width: 0; + } + + .invoice-summary-metric-label { + font-size: 0.66rem; + font-weight: 700; + letter-spacing: 0.1em; + text-transform: uppercase; + color: #64748b; + line-height: 1.35; + } + + .invoice-summary-metric-value { + font-size: 1rem; + font-weight: 700; + color: #0f172a; + line-height: 1.25; + word-break: break-word; + } + + .invoice-summary-metric--accent { + border-color: #fecaca; + background: #fff5f5; + } + + .invoice-summary-metric--accent .invoice-summary-metric-value { + color: #b91c1c; + } + + .invoice-summary-metric--total { + border-color: #bfdbfe; + background: linear-gradient(180deg, #eff6ff 0%, #dbeafe 100%); + } + + .invoice-summary-metric--total .invoice-summary-metric-value { + font-size: 1.22rem; + font-weight: 800; + color: #1d4ed8; + } + + .invoice-payment-compact { + margin-top: 1rem; + padding-top: 0.95rem; + border-top: 1px solid #dbe5f0; + } + + .invoice-payment-list { + display: flex; + flex-wrap: wrap; + gap: 0.55rem; + } + + .invoice-payment-pill { + display: inline-flex; + align-items: center; + flex-wrap: wrap; + gap: 0.35rem; + padding: 0.45rem 0.72rem; + border-radius: 999px; + background: #e2e8f0; + color: #334155; + font-size: 0.76rem; + font-weight: 600; + line-height: 1.35; + } + + .invoice-payment-pill strong { + font-size: 0.82rem; + font-weight: 700; + color: #0f172a; + } + + .invoice-payment-sep { + color: #94a3b8; + } + + .invoice-summary-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; + padding: 0.45rem 0; + font-size: 0.92rem; + color: #334155; + } + + .invoice-summary-row span:last-child { + font-weight: 700; + color: #0f172a; + } + + .invoice-summary-row--accent span:last-child { + color: #b91c1c; + } + + .invoice-summary-row--total { + margin-top: 0.25rem; + padding-top: 0.85rem; + border-top: 1px solid #cbd5e1; + font-size: 1.02rem; + font-weight: 700; + } + + .invoice-summary-row--total span:last-child { + font-size: 1.35rem; + font-weight: 800; + color: #1d4ed8; + } + + .invoice-payment-table { + margin-top: 0.75rem; + } + + .invoice-payment-table thead th { + background: #e2e8f0; + color: #334155; + border-color: #dbe5f0; + font-size: 0.68rem; + font-weight: 700; + letter-spacing: 0.1em; + text-transform: uppercase; + } + + .invoice-payment-table tbody td { + font-size: 0.82rem; + color: #475569; + border-color: #e2e8f0; + } + + .invoice-terms-list { + margin: 0; + padding-left: 1.15rem; + color: #64748b; + font-size: 0.84rem; + } + + .invoice-terms-list li + li { + margin-top: 0.4rem; + } + + .invoice-footer { + margin-top: 1.5rem; + padding-top: 1.25rem; + border-top: 1px solid #dbe5f0; + } + + .invoice-signature-line { + width: 180px; + margin: 0 auto; + padding-top: 0.55rem; + border-top: 1px solid #94a3b8; + } + + .invoice-qr-card { + display: inline-flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 0.45rem; + min-width: 132px; + min-height: 132px; + padding: 0.75rem; + border: 1px solid #dbe5f0; + border-radius: 18px; + background: #ffffff; + } + + .invoice-footer-note { + font-size: 0.78rem; + color: #64748b; + } + + .invoice-currency { + display: inline-block; + margin-inline-end: 0.22rem; + font-size: 0.72em; + font-weight: 600; + color: #64748b; + } + + .x-small { + font-size: 0.72rem; + } + + @media (min-width: 768px) { + .invoice-pill-group { + justify-content: flex-end; + } + } + + @media (min-width: 992px) { + .invoice-summary-grid { + grid-template-columns: repeat(3, minmax(0, 1fr)); + } + } + + @media (max-width: 767.98px) { + .invoice-print-shell { + padding: 0.85rem; + } + + .invoice-paper-body { + padding: 1.25rem; + } + + .invoice-title { + font-size: 1.18rem; + } + + .invoice-meta-grid { + grid-template-columns: 1fr; + } + } -