From 110b26742e802735cc6203d0f961ad2e6e5dfe3a Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Tue, 24 Feb 2026 08:21:47 +0000 Subject: [PATCH] new updates --- admin/order_view.php | 120 ++++++++++- admin/orders.php | 1 + admin/products.php | 192 ++++++++++++------ api/create_customer.php | 5 +- api/customer_loyalty_history.php | 26 +++ api/order.php | 26 ++- api/translate.php | 29 +++ assets/css/custom.css | 16 +- assets/js/main.js | 120 +++++++---- db/migrations/027_loyalty_history.sql | 11 + db/migrations/028_add_name_ar_to_products.sql | 2 + pos.php | 62 +++++- 12 files changed, 491 insertions(+), 119 deletions(-) create mode 100644 api/customer_loyalty_history.php create mode 100644 api/translate.php create mode 100644 db/migrations/027_loyalty_history.sql create mode 100644 db/migrations/028_add_name_ar_to_products.sql diff --git a/admin/order_view.php b/admin/order_view.php index 7f4655b..910dee5 100644 --- a/admin/order_view.php +++ b/admin/order_view.php @@ -47,9 +47,94 @@ foreach ($items as $item) { } $vat_or_discount = (float)$order['discount']; +$company_settings = get_company_settings(); ?> -
+ + + + + +

Order #

Placed on

@@ -74,7 +159,7 @@ $vat_or_discount = (float)$order['discount'];
-
+
Order Items
@@ -129,17 +214,17 @@ $vat_or_discount = (float)$order['discount'];
-
+
Internal Notes
-

+

-
+
Order Status
@@ -194,7 +279,7 @@ $vat_or_discount = (float)$order['discount'];
Customer Information
-
+
@@ -204,19 +289,28 @@ $vat_or_discount = (float)$order['discount'];
- + + Phone:
-
- +
+ + Email:
+ +
+ + Address: + +
+
- +

No customer attached to this order (Guest)

@@ -267,6 +361,11 @@ function printThermalReceipt() { const win = window.open('', 'Receipt', `width=${width},height=${height},top=${top},left=${left}`); + if (!win) { + alert('Please allow popups for this website to print thermal receipts.'); + return; + } + const tr = { 'Order': 'الطلب', 'Type': 'النوع', @@ -278,6 +377,7 @@ function printThermalReceipt() { 'TOTAL': 'المجموع', 'Subtotal': 'المجموع الفرعي', 'VAT': 'ضريبة القيمة المضافة', + 'Tax Included': 'شامل الضريبة', 'THANK YOU FOR YOUR VISIT!': 'شكراً لزيارتكم!', 'Please come again.': 'يرجى زيارتنا مرة أخرى.', 'Customer Details': 'تفاصيل العميل', diff --git a/admin/orders.php b/admin/orders.php index 5a42bd9..d424773 100644 --- a/admin/orders.php +++ b/admin/orders.php @@ -312,6 +312,7 @@ include 'includes/header.php'; 'cash' => 'bg-success', 'credit card' => 'bg-primary', 'loyalty redeem' => 'bg-warning', + 'bank transfer' => 'bg-info', 'unpaid' => 'bg-secondary', default => 'bg-secondary' }; diff --git a/admin/products.php b/admin/products.php index f73280a..1068d9b 100644 --- a/admin/products.php +++ b/admin/products.php @@ -11,60 +11,76 @@ $message = ''; if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) { $action = $_POST['action']; $id = isset($_POST['id']) ? (int)$_POST['id'] : null; - $name = $_POST['name']; - $category_id = $_POST['category_id']; - $price = $_POST['price']; - $cost_price = $_POST['cost_price'] ?: 0; - $stock_quantity = $_POST['stock_quantity'] ?: 0; - $description = $_POST['description']; - - $promo_discount_percent = $_POST['promo_discount_percent'] !== '' ? $_POST['promo_discount_percent'] : null; - $promo_date_from = $_POST['promo_date_from'] !== '' ? $_POST['promo_date_from'] : null; - $promo_date_to = $_POST['promo_date_to'] !== '' ? $_POST['promo_date_to'] : null; - $image_url = null; - if ($id) { - $stmt = $pdo->prepare("SELECT image_url FROM products WHERE id = ?"); - $stmt->execute([$id]); - $image_url = $stmt->fetchColumn(); + if ($action === 'cancel_promotion' && $id) { + if (!has_permission('products_edit')) { + $message = '
Access Denied: You do not have permission to edit products.
'; + } else { + try { + $stmt = $pdo->prepare("UPDATE products SET promo_discount_percent = NULL, promo_date_from = NULL, promo_date_to = NULL WHERE id = ?"); + $stmt->execute([$id]); + $message = '
Promotion cancelled successfully!
'; + } catch (PDOException $e) { + $message = '
Database error: ' . $e->getMessage() . '
'; + } + } } else { - $image_url = 'https://placehold.co/400x300?text=' . urlencode($name); - } - - if (isset($_FILES['image']) && $_FILES['image']['error'] === UPLOAD_ERR_OK) { - $uploadDir = __DIR__ . '/../assets/images/products/'; - if (!is_dir($uploadDir)) mkdir($uploadDir, 0755, true); + $name = $_POST['name']; + $name_ar = $_POST['name_ar'] ?? ''; + $category_id = $_POST['category_id']; + $price = $_POST['price']; + $cost_price = $_POST['cost_price'] ?: 0; + $stock_quantity = $_POST['stock_quantity'] ?: 0; + $description = $_POST['description']; - $file_ext = strtolower(pathinfo($_FILES['image']['name'], PATHINFO_EXTENSION)); - if (in_array($file_ext, ['jpg', 'jpeg', 'png', 'gif', 'webp'])) { - $fileName = uniqid('prod_') . '.' . $file_ext; - if (move_uploaded_file($_FILES['image']['tmp_name'], $uploadDir . $fileName)) { - $image_url = 'assets/images/products/' . $fileName; - } - } - } + $promo_discount_percent = $_POST['promo_discount_percent'] !== '' ? $_POST['promo_discount_percent'] : null; + $promo_date_from = $_POST['promo_date_from'] !== '' ? $_POST['promo_date_from'] : null; + $promo_date_to = $_POST['promo_date_to'] !== '' ? $_POST['promo_date_to'] : null; - try { - if ($action === 'edit_product' && $id) { - // Check for edit OR add (for backward compatibility) - if (!has_permission('products_edit') && !has_permission('products_add')) { - $message = '
Access Denied: You do not have permission to edit products.
'; - } else { - $stmt = $pdo->prepare("UPDATE products SET name = ?, category_id = ?, price = ?, cost_price = ?, stock_quantity = ?, description = ?, image_url = ?, promo_discount_percent = ?, promo_date_from = ?, promo_date_to = ? WHERE id = ?"); - $stmt->execute([$name, $category_id, $price, $cost_price, $stock_quantity, $description, $image_url, $promo_discount_percent, $promo_date_from, $promo_date_to, $id]); - $message = '
Product updated successfully!
'; - } - } elseif ($action === 'add_product') { - if (!has_permission('products_add')) { - $message = '
Access Denied: You do not have permission to add products.
'; - } else { - $stmt = $pdo->prepare("INSERT INTO products (name, category_id, price, cost_price, stock_quantity, description, image_url, promo_discount_percent, promo_date_from, promo_date_to) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"); - $stmt->execute([$name, $category_id, $price, $cost_price, $stock_quantity, $description, $image_url, $promo_discount_percent, $promo_date_from, $promo_date_to]); - $message = '
Product created successfully!
'; + $image_url = null; + if ($id) { + $stmt = $pdo->prepare("SELECT image_url FROM products WHERE id = ?"); + $stmt->execute([$id]); + $image_url = $stmt->fetchColumn(); + } else { + $image_url = 'https://placehold.co/400x300?text=' . urlencode($name); + } + + if (isset($_FILES['image']) && $_FILES['image']['error'] === UPLOAD_ERR_OK) { + $uploadDir = __DIR__ . '/../assets/images/products/'; + if (!is_dir($uploadDir)) mkdir($uploadDir, 0755, true); + + $file_ext = strtolower(pathinfo($_FILES['image']['name'], PATHINFO_EXTENSION)); + if (in_array($file_ext, ['jpg', 'jpeg', 'png', 'gif', 'webp'])) { + $fileName = uniqid('prod_') . '.' . $file_ext; + if (move_uploaded_file($_FILES['image']['tmp_name'], $uploadDir . $fileName)) { + $image_url = 'assets/images/products/' . $fileName; + } } } - } catch (PDOException $e) { - $message = '
Database error: ' . $e->getMessage() . '
'; + + try { + if ($action === 'edit_product' && $id) { + // Check for edit OR add (for backward compatibility) + if (!has_permission('products_edit') && !has_permission('products_add')) { + $message = '
Access Denied: You do not have permission to edit products.
'; + } else { + $stmt = $pdo->prepare("UPDATE products SET name = ?, name_ar = ?, category_id = ?, price = ?, cost_price = ?, stock_quantity = ?, description = ?, image_url = ?, promo_discount_percent = ?, promo_date_from = ?, promo_date_to = ? WHERE id = ?"); + $stmt->execute([$name, $name_ar, $category_id, $price, $cost_price, $stock_quantity, $description, $image_url, $promo_discount_percent, $promo_date_from, $promo_date_to, $id]); + $message = '
Product updated successfully!
'; + } + } elseif ($action === 'add_product') { + if (!has_permission('products_add')) { + $message = '
Access Denied: You do not have permission to add products.
'; + } else { + $stmt = $pdo->prepare("INSERT INTO products (name, name_ar, category_id, price, cost_price, stock_quantity, description, image_url, promo_discount_percent, promo_date_from, promo_date_to) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"); + $stmt->execute([$name, $name_ar, $category_id, $price, $cost_price, $stock_quantity, $description, $image_url, $promo_discount_percent, $promo_date_from, $promo_date_to]); + $message = '
Product created successfully!
'; + } + } + } catch (PDOException $e) { + $message = '
Database error: ' . $e->getMessage() . '
'; + } } } @@ -92,7 +108,8 @@ $query = "SELECT p.*, c.name as category_name LEFT JOIN categories c ON p.category_id = c.id"; if ($search) { - $where[] = "(p.name LIKE ? OR p.description LIKE ?)"; + $where[] = "(p.name LIKE ? OR p.name_ar LIKE ? OR p.description LIKE ?)"; + $params[] = "%$search%"; $params[] = "%$search%"; $params[] = "%$search%"; } @@ -173,6 +190,7 @@ include 'includes/header.php'; !empty($product['promo_date_to']) && $today >= $product['promo_date_from'] && $today <= $product['promo_date_to']; + $has_promo_data = !empty($product['promo_discount_percent']); ?> @@ -180,6 +198,9 @@ include 'includes/header.php';
+ +
+
@@ -200,13 +221,25 @@ include 'includes/header.php'; - - -% - - Inactive - - - - +
+ + -% + + Inactive + + - + + + +
+ + + +
+ +
@@ -249,9 +282,20 @@ include 'includes/header.php';
- +
+
+ +
+ +
+
@@ -341,6 +385,7 @@ function prepareEditForm(prod) { document.getElementById('productAction').value = 'edit_product'; document.getElementById('productId').value = prod.id; document.getElementById('productName').value = prod.name; + document.getElementById('productNameAr').value = prod.name_ar || ''; document.getElementById('productCategoryId').value = prod.category_id; document.getElementById('productPrice').value = prod.price; document.getElementById('productCostPrice').value = prod.cost_price || ''; @@ -358,6 +403,39 @@ function prepareEditForm(prod) { document.getElementById('productImagePreview').style.display = 'none'; } } + +async function translateName() { + const enName = document.getElementById('productName').value; + if (!enName) { + alert('Please enter an English name first.'); + return; + } + + const btn = document.getElementById('translateBtn'); + const originalContent = btn.innerHTML; + btn.innerHTML = ' Translating...'; + btn.classList.add('disabled'); + + try { + const response = await fetch('../api/translate.php', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ text: enName, target_lang: 'Arabic' }) + }); + const data = await response.json(); + if (data.success) { + document.getElementById('productNameAr').value = data.translated_text; + } else { + alert('Translation error: ' + data.error); + } + } catch (error) { + console.error('Translation error:', error); + alert('An error occurred during translation.'); + } finally { + btn.innerHTML = originalContent; + btn.classList.remove('disabled'); + } +} diff --git a/api/create_customer.php b/api/create_customer.php index f41441f..3027178 100644 --- a/api/create_customer.php +++ b/api/create_customer.php @@ -18,8 +18,9 @@ if (empty($name)) { exit; } -if (!preg_match('/^\d{8}$/', $phone)) { - echo json_encode(['error' => 'Phone number must be exactly 8 digits']); +// Relaxed phone validation: 8 to 15 digits +if (!preg_match('/^\d{8,15}$/', $phone)) { + echo json_encode(['error' => 'Phone number must be between 8 and 15 digits']); exit; } diff --git a/api/customer_loyalty_history.php b/api/customer_loyalty_history.php new file mode 100644 index 0000000..f2ec084 --- /dev/null +++ b/api/customer_loyalty_history.php @@ -0,0 +1,26 @@ + false, 'error' => 'Customer ID required']); + exit; +} + +try { + $pdo = db(); + $stmt = $pdo->prepare("SELECT id, points_change, reason, order_id, created_at + FROM loyalty_points_history + WHERE customer_id = ? + ORDER BY created_at DESC + LIMIT 50"); + $stmt->execute([$customer_id]); + $history = $stmt->fetchAll(PDO::FETCH_ASSOC); + + echo json_encode(['success' => true, 'history' => $history]); +} catch (Exception $e) { + echo json_encode(['success' => false, 'error' => $e->getMessage()]); +} diff --git a/api/order.php b/api/order.php index 3b4b286..feb56b4 100644 --- a/api/order.php +++ b/api/order.php @@ -80,6 +80,8 @@ try { $current_points = 0; $points_deducted = 0; $points_awarded = 0; + $award_history_id = null; + $redeem_history_id = null; if ($customer_id) { $stmt = $pdo->prepare("SELECT name, phone, points FROM customers WHERE id = ?"); @@ -105,6 +107,11 @@ try { $pdo->prepare("UPDATE customers SET loyalty_redemptions_count = loyalty_redemptions_count + 1 WHERE id = ?")->execute([$customer_id]); $deductStmt->execute([$points_threshold, $customer_id]); $points_deducted = $points_threshold; + + // Record Loyalty History (Deduction) + $historyStmt = $pdo->prepare("INSERT INTO loyalty_points_history (customer_id, points_change, reason) VALUES (?, ?, 'Redeemed Free Meal')"); + $historyStmt->execute([$customer_id, -$points_threshold]); + $redeem_history_id = $pdo->lastInsertId(); // --- OVERRIDE PAYMENT TYPE --- $ptStmt = $pdo->prepare("SELECT id FROM payment_types WHERE name = 'Loyalty Redeem' LIMIT 1"); @@ -115,11 +122,16 @@ try { } } - // Award Points - if ($customer_id && $loyalty_enabled) { + // Award Points (ONLY IF NOT REDEEMING) + if ($customer_id && $loyalty_enabled && !$redeem_loyalty) { $awardStmt = $pdo->prepare("UPDATE customers SET points = points + ? WHERE id = ?"); $awardStmt->execute([$points_per_order, $customer_id]); $points_awarded = $points_per_order; + + // Record Loyalty History (Award) + $historyStmt = $pdo->prepare("INSERT INTO loyalty_points_history (customer_id, points_change, reason) VALUES (?, ?, 'Earned from Order')"); + $historyStmt->execute([$customer_id, $points_per_order]); + $award_history_id = $pdo->lastInsertId(); } // User/Payment info @@ -214,6 +226,16 @@ try { $order_id = $pdo->lastInsertId(); } + // Update loyalty history with order_id + if ($order_id) { + if ($award_history_id) { + $pdo->prepare("UPDATE loyalty_points_history SET order_id = ? WHERE id = ?")->execute([$order_id, $award_history_id]); + } + if ($redeem_history_id) { + $pdo->prepare("UPDATE loyalty_points_history SET order_id = ? WHERE id = ?")->execute([$order_id, $redeem_history_id]); + } + } + // Insert Items and Update Stock $item_stmt = $pdo->prepare("INSERT INTO order_items (order_id, product_id, variant_id, quantity, unit_price) VALUES (?, ?, ?, ?, ?)"); $stock_stmt = $pdo->prepare("UPDATE products SET stock_quantity = stock_quantity - ? WHERE id = ?"); diff --git a/api/translate.php b/api/translate.php new file mode 100644 index 0000000..ae368e8 --- /dev/null +++ b/api/translate.php @@ -0,0 +1,29 @@ + false, 'error' => 'No text provided']); + exit; +} + +$prompt = "Translate the following product name or description to $target_lang. Return ONLY the translated text, nothing else.\n\nText: $text"; + +$resp = LocalAIApi::createResponse([ + 'input' => [ + ['role' => 'system', 'content' => 'You are a helpful translation assistant.'], + ['role' => 'user', 'content' => $prompt], + ], +]); + +if (!empty($resp['success'])) { + $translatedText = LocalAIApi::extractText($resp); + echo json_encode(['success' => true, 'translated_text' => trim($translatedText)]); +} else { + echo json_encode(['success' => false, 'error' => $resp['error'] ?? 'AI error']); +} + diff --git a/assets/css/custom.css b/assets/css/custom.css index 0f9113c..1a0d88b 100644 --- a/assets/css/custom.css +++ b/assets/css/custom.css @@ -125,6 +125,20 @@ body { user-select: none; } +/* Force group elements to always be black and ignore theme active/hover colors */ +.sidebar .collapse .nav-link, +.sidebar .collapse .nav-link.active, +.sidebar .collapse .nav-link:hover, +.sidebar .collapse .nav-link:focus { + color: #000000 !important; +} +.sidebar .collapse .nav-link i, +.sidebar .collapse .nav-link.active i, +.sidebar .collapse .nav-link:hover i, +.sidebar .collapse .nav-link:focus i { + color: #000000 !important; +} + .sidebar-heading i { color: var(--accent-color); font-size: 1.1em; @@ -415,4 +429,4 @@ body { /* Ensure Dropdowns are always on top */ .dropdown-menu { z-index: 1050 !important; -} \ No newline at end of file +} diff --git a/assets/js/main.js b/assets/js/main.js index 004bec4..d547d9c 100644 --- a/assets/js/main.js +++ b/assets/js/main.js @@ -36,6 +36,13 @@ document.addEventListener('DOMContentLoaded', () => { const loyaltyPointsDisplay = document.getElementById('loyalty-points-display'); const loyaltyMessage = document.getElementById('loyalty-message'); const redeemLoyaltyBtn = document.getElementById('redeem-loyalty-btn'); + const viewPointsHistoryBtn = document.getElementById('view-points-history-btn'); + + // Points History Modal + const pointsHistoryModalEl = document.getElementById('pointsHistoryModal'); + const pointsHistoryModal = pointsHistoryModalEl ? new bootstrap.Modal(pointsHistoryModalEl) : null; + const pointsHistoryBody = document.getElementById('points-history-body'); + const pointsHistoryEmpty = document.getElementById('points-history-empty'); // Table Management let currentTableId = null; @@ -188,7 +195,9 @@ document.addEventListener('DOMContentLoaded', () => { // Populate Cart cart = data.items; // Assuming format matches - cartVatInput.value = data.order.discount || 0; // We still use the discount field for VAT value + + // Note: In auto-VAT mode, we don't load data.order.discount into cartVatInput + // as it will be re-calculated based on subtotal. updateCart(); if (recallModal) recallModal.hide(); @@ -287,7 +296,6 @@ document.addEventListener('DOMContentLoaded', () => { // Hide Loyalty if (loyaltySection) loyaltySection.classList.add('d-none'); isLoyaltyRedemption = false; - cartVatInput.value = 0; updateCart(); customerSearchInput.focus(); @@ -304,9 +312,9 @@ document.addEventListener('DOMContentLoaded', () => { return; } - // --- NEW RESTRICTION --- + // --- STRICT ONE ITEM RESTRICTION --- if (cart.length > 1) { - showToast("Can only redeem a free meal with a single item in cart!", "warning"); + showToast("Can only redeem a free meal with exactly one item in cart!", "warning"); return; } @@ -314,29 +322,71 @@ document.addEventListener('DOMContentLoaded', () => { Swal.fire({ title: 'Redeem Loyalty?', - text: "Redeem 70 points for a free meal? This will apply a full discount to the current order.", + text: "Redeem points for a free meal? This will finalize the order immediately.", icon: 'question', showCancelButton: true, confirmButtonColor: '#198754', cancelButtonColor: '#6c757d', - confirmButtonText: 'Yes, redeem it!' + confirmButtonText: 'Yes, redeem and finish!' }).then((result) => { if (result.isConfirmed) { isLoyaltyRedemption = true; - - // Calculate total and apply as discount (which is now negative VAT internally) - const subtotal = cart.reduce((acc, item) => acc + (item.price * item.quantity), 0); - cartVatInput.value = -subtotal.toFixed(2); updateCart(); - showToast("Loyalty Redemption Applied!", "success"); - redeemLoyaltyBtn.disabled = true; // Prevent double click - loyaltyMessage.innerHTML = ' Redeemed! Place order to finalize.'; + // Directly process order with Loyalty payment type + processOrder(null, 'Loyalty Redeem'); } }); }); } + // Points History View + if (viewPointsHistoryBtn) { + viewPointsHistoryBtn.addEventListener('click', () => { + if (!currentCustomer) return; + fetchPointsHistory(currentCustomer.id); + }); + } + + function fetchPointsHistory(customerId) { + if (!pointsHistoryBody || !pointsHistoryModal) return; + pointsHistoryBody.innerHTML = '
Loading...'; + pointsHistoryEmpty.classList.add('d-none'); + pointsHistoryModal.show(); + + fetch(`api/customer_loyalty_history.php?customer_id=${customerId}`) + .then(res => res.json()) + .then(data => { + pointsHistoryBody.innerHTML = ''; + if (data.success && data.history.length > 0) { + data.history.forEach(item => { + const tr = document.createElement('tr'); + const dateObj = new Date(item.created_at); + const date = dateObj.toLocaleDateString('en-GB', { day: '2-digit', month: 'short' }) + ' ' + + dateObj.toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit' }); + + const pointsClass = item.points_change > 0 ? 'text-success' : 'text-danger'; + const pointsPrefix = item.points_change > 0 ? '+' : ''; + + tr.innerHTML = ` + ${date} + +
${item.reason}
+ ${item.order_id ? `
Order #${item.order_id}
` : ''} + + ${pointsPrefix}${item.points_change} + `; + pointsHistoryBody.appendChild(tr); + }); + } else { + pointsHistoryEmpty.classList.remove('d-none'); + } + }) + .catch(err => { + pointsHistoryBody.innerHTML = 'Error loading history'; + }); + } + // --- Table & Order Type Logic --- const orderTypeInputs = document.querySelectorAll('input[name="order_type"]'); @@ -434,6 +484,7 @@ document.addEventListener('DOMContentLoaded', () => { const product = { id: target.dataset.id, name: target.dataset.name, + name_ar: target.dataset.nameAr || '', price: parseFloat(target.dataset.price), base_price: parseFloat(target.dataset.price), hasVariants: target.dataset.hasVariants === 'true', @@ -517,6 +568,7 @@ document.addEventListener('DOMContentLoaded', () => {

Cart is empty

`; cartSubtotal.innerText = formatCurrency(0); + cartVatInput.value = 0; cartTotalPrice.innerText = formatCurrency(0); if (quickOrderBtn) quickOrderBtn.disabled = true; if (placeOrderBtn) placeOrderBtn.disabled = true; @@ -534,10 +586,12 @@ document.addEventListener('DOMContentLoaded', () => { row.className = 'd-flex justify-content-between align-items-center mb-3 border-bottom pb-2'; const variantLabel = item.variant_name ? `${item.variant_name}` : ''; + const arabicNameDisplay = item.name_ar ? `
${item.name_ar}
` : ''; row.innerHTML = `
${item.name}
+ ${arabicNameDisplay}
${formatCurrency(item.price)} ${variantLabel}
@@ -555,13 +609,18 @@ document.addEventListener('DOMContentLoaded', () => { cartSubtotal.innerText = formatCurrency(subtotal); - let vat = parseFloat(cartVatInput.value) || 0; + let vat = 0; if (isLoyaltyRedemption) { // Internal trick: send negative VAT to represent discount for loyalty vat = -subtotal; - cartVatInput.value = (-subtotal).toFixed(2); + } else { + // Automatic VAT calculation from system settings + const vatRate = parseFloat(COMPANY_SETTINGS.vat_rate) || 0; + vat = subtotal * (vatRate / 100); } + cartVatInput.value = vat.toFixed(2); + let total = subtotal + vat; if (total < 0) total = 0; @@ -570,12 +629,6 @@ document.addEventListener('DOMContentLoaded', () => { if (placeOrderBtn) placeOrderBtn.disabled = false; } - if (cartVatInput) { - cartVatInput.addEventListener('input', () => { - updateCart(); - }); - } - window.removeFromCart = function(index) { cart.splice(index, 1); updateCart(); @@ -631,6 +684,7 @@ document.addEventListener('DOMContentLoaded', () => { if (n.includes('cash')) return 'bi-cash-coin'; if (n.includes('card') || n.includes('visa') || n.includes('master')) return 'bi-credit-card'; if (n.includes('qr') || n.includes('scan')) return 'bi-qr-code'; + if (n.includes('bank') || n.includes('transfer')) return 'bi-building-columns'; return 'bi-wallet2'; } @@ -812,6 +866,11 @@ document.addEventListener('DOMContentLoaded', () => { const win = window.open('', 'Receipt', `width=${width},height=${height},top=${top},left=${left}`); + if (!win) { + alert('Please allow popups for this website to print receipts.'); + return; + } + const tr = { 'Order': 'الطلب', 'Type': 'النوع', @@ -839,6 +898,7 @@ document.addEventListener('DOMContentLoaded', () => {
${item.name}
+ ${item.name_ar ? `
${item.name_ar}
` : ''} ${item.variant_name ? `
(${item.variant_name})
` : ''}
${item.quantity} x ${formatCurrency(item.price)}
@@ -874,18 +934,16 @@ document.addEventListener('DOMContentLoaded', () => { const loyaltyHtml = data.loyaltyRedeemed ? `
* Loyalty Reward Applied *
` : ''; const subtotal = data.total - data.vat; - const vatRateSettings = parseFloat(settings.vat_rate) || 0; - const vatAmount = data.vat; // User manually entered VAT - const logoHtml = settings.logo_url ? `` : ''; const html = ` Receipt #${data.orderId} +