diff --git a/api/order.php b/api/order.php index e1833bf..1ff66bf 100644 --- a/api/order.php +++ b/api/order.php @@ -141,6 +141,8 @@ try { if ($product['is_loyalty']) { $loyalty_items_indices[] = count($processed_items); + } elseif ($redeem_loyalty) { + throw new Exception("Redemption is only allowed when all items in the cart are loyalty-eligible."); } $processed_items[] = [ @@ -155,35 +157,35 @@ try { } } - // Handle Loyalty Redemption (Make one loyalty item free) + // Handle Loyalty Redemption (Make loyalty items free up to points limit) if ($redeem_loyalty && $customer_id) { if (empty($loyalty_items_indices)) { throw new Exception("No loyalty-eligible products in the order to redeem."); } - // Find the most expensive loyalty item - $max_price = -1; - $max_index = -1; - foreach ($loyalty_items_indices as $idx) { - if ($processed_items[$idx]['unit_price'] > $max_price) { - $max_price = $processed_items[$idx]['unit_price']; - $max_index = $idx; + $possible_redemptions = floor($current_points / $points_threshold); + $redemptions_done = 0; + + while ($redemptions_done < $possible_redemptions) { + // Find the most expensive loyalty item that is still paid + $max_price = -1; + $max_index = -1; + foreach ($processed_items as $idx => $item) { + if ($item['is_loyalty'] && $item['unit_price'] > 0 && $item['quantity'] > 0) { + if ($item['unit_price'] > $max_price) { + $max_price = $item['unit_price']; + $max_index = $idx; + } + } } - } - if ($max_index !== -1) { - // Deduct points from customer - $deductStmt = $pdo->prepare("UPDATE customers SET points = points - ? WHERE id = ?"); - $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; + if ($max_index === -1) break; // No more loyalty items to redeem - // Record Loyalty History (Deduction) - $historyStmt = $pdo->prepare("INSERT INTO loyalty_points_history (customer_id, points_change, reason) VALUES (?, ?, 'Redeemed Free Product')"); - $historyStmt->execute([$customer_id, -$points_threshold]); - $redeem_history_id = $pdo->lastInsertId(); + // Deduct points for ONE redemption + $points_deducted += $points_threshold; + $redemptions_done++; - // Make ONE unit of the most expensive loyalty product free + // Make ONE unit of the found product free if ($processed_items[$max_index]['quantity'] > 1) { $processed_items[$max_index]['quantity']--; $free_item = $processed_items[$max_index]; @@ -193,6 +195,19 @@ try { } else { $processed_items[$max_index]['unit_price'] = 0; } + } + + if ($redemptions_done > 0) { + // Update customer points and count + $deductStmt = $pdo->prepare("UPDATE customers SET points = points - ? WHERE id = ?"); + $deductStmt->execute([$points_deducted, $customer_id]); + $pdo->prepare("UPDATE customers SET loyalty_redemptions_count = loyalty_redemptions_count + ? WHERE id = ?") + ->execute([$redemptions_done, $customer_id]); + + // Record Loyalty History (Deduction) + $historyStmt = $pdo->prepare("INSERT INTO loyalty_points_history (customer_id, points_change, reason) VALUES (?, ?, 'Redeemed Free Product(s)')"); + $historyStmt->execute([$customer_id, -$points_deducted]); + $redeem_history_id = $pdo->lastInsertId(); // --- OVERRIDE PAYMENT TYPE --- $ptStmt = $pdo->prepare("SELECT id FROM payment_types WHERE name = 'Loyalty Redeem' LIMIT 1"); @@ -201,6 +216,8 @@ try { if ($loyaltyPt) { $data['payment_type_id'] = $loyaltyPt; } + } else { + throw new Exception("No loyalty-eligible products in the order to redeem."); } } @@ -208,14 +225,12 @@ try { foreach ($processed_items as $pi) { $calculated_total += $pi['unit_price'] * $pi['quantity']; // Award points for PAID loyalty items - if ($pi['is_loyalty'] && $pi['unit_price'] > 0) { + if ($pi['is_loyalty'] && $pi['unit_price'] > 0 && $pi['quantity'] > 0) { $points_awarded += $pi['quantity'] * $points_per_product; } } - // Award Points (ONLY IF NOT REDEEMING or we can award for remaining items) - // User said "calculate these products multiply 10 points and will be calculated to the customer" - // Usually points are earned on the whole order, but here it's specifically per product. + // Award Points if ($customer_id && $loyalty_enabled && $points_awarded > 0) { $awardStmt = $pdo->prepare("UPDATE customers SET points = points + ? WHERE id = ?"); $awardStmt->execute([$points_awarded, $customer_id]); diff --git a/api/recall_orders.php b/api/recall_orders.php index b27425f..4641174 100644 --- a/api/recall_orders.php +++ b/api/recall_orders.php @@ -47,9 +47,9 @@ try { exit; } - // Fetch Items + // Fetch Items with is_loyalty $stmtItems = $pdo->prepare(" - SELECT oi.*, p.name as product_name, p.price as base_price, v.name as variant_name, v.price_adjustment + SELECT oi.*, p.name as product_name, p.price as base_price, p.is_loyalty, v.name as variant_name, v.price_adjustment FROM order_items oi JOIN products p ON oi.product_id = p.id LEFT JOIN product_variants v ON oi.variant_id = v.id @@ -65,13 +65,12 @@ try { 'id' => $item['product_id'], 'name' => $item['product_name'], 'price' => floatval($item['unit_price']), - 'base_price' => floatval($item['base_price']), // Note: this might be different from current DB price if changed, but for recall we usually want the ORIGINAL price or CURRENT? - // Ideally, if we edit an order, we might want to keep original prices OR update to current. - // For simplicity, let's use the price stored in order_items (unit_price) as the effective price. + 'base_price' => floatval($item['base_price']), 'quantity' => intval($item['quantity']), 'variant_id' => $item['variant_id'], 'variant_name' => $item['variant_name'], - 'hasVariants' => !empty($item['variant_id']) + 'hasVariants' => !empty($item['variant_id']), + 'is_loyalty' => intval($item['is_loyalty']) === 1 ]; } @@ -81,6 +80,18 @@ try { $cStmt = $pdo->prepare("SELECT * FROM customers WHERE id = ?"); $cStmt->execute([$order['customer_id']]); $customer = $cStmt->fetch(PDO::FETCH_ASSOC); + + if ($customer) { + // Fetch Loyalty Threshold for consistent frontend logic + $settingsStmt = $pdo->query("SELECT points_for_free_meal FROM loyalty_settings WHERE id = 1"); + $loyaltySettings = $settingsStmt->fetch(PDO::FETCH_ASSOC); + $threshold = $loyaltySettings ? intval($loyaltySettings['points_for_free_meal']) : 70; + + $customer['points'] = intval($customer['points']); + $customer['eligible_for_free_meal'] = $customer['points'] >= $threshold; + $customer['eligible_count'] = floor($customer['points'] / $threshold); + $customer['points_needed'] = max(0, $threshold - ($customer['points'] % $threshold)); + } } echo json_encode([ diff --git a/api/search_customers.php b/api/search_customers.php index 2a8ef78..aca5ad2 100644 --- a/api/search_customers.php +++ b/api/search_customers.php @@ -15,7 +15,7 @@ try { // Fetch Loyalty Settings $settingsStmt = $pdo->query("SELECT points_for_free_meal FROM loyalty_settings WHERE id = 1"); $settings = $settingsStmt->fetch(PDO::FETCH_ASSOC); - $threshold = $settings ? $settings['points_for_free_meal'] : 70; + $threshold = $settings ? intval($settings['points_for_free_meal']) : 70; $stmt = $pdo->prepare("SELECT id, name, phone, email, points FROM customers WHERE name LIKE ? OR phone LIKE ? LIMIT 10"); $searchTerm = "%$q%"; @@ -25,7 +25,8 @@ try { foreach ($customers as &$customer) { $customer['points'] = intval($customer['points']); $customer['eligible_for_free_meal'] = $customer['points'] >= $threshold; - $customer['points_needed'] = max(0, $threshold - $customer['points']); + $customer['eligible_count'] = floor($customer['points'] / $threshold); + $customer['points_needed'] = max(0, $threshold - ($customer['points'] % $threshold)); $customer['threshold'] = $threshold; } @@ -33,4 +34,4 @@ try { } catch (Exception $e) { error_log("Customer Search Error: " . $e->getMessage()); echo json_encode(['error' => 'Database error']); -} \ No newline at end of file +} diff --git a/assets/js/main.js b/assets/js/main.js index 664a8f1..c13533d 100644 --- a/assets/js/main.js +++ b/assets/js/main.js @@ -251,6 +251,39 @@ document.addEventListener('DOMContentLoaded', () => { }); } + function updateLoyaltyUI() { + if (!loyaltySection || typeof LOYALTY_SETTINGS === 'undefined' || !LOYALTY_SETTINGS.is_enabled) return; + + if (currentCustomer) { + loyaltySection.classList.remove('d-none'); + if (loyaltyPointsDisplay) loyaltyPointsDisplay.textContent = currentCustomer.points + ' pts'; + + if (redeemLoyaltyBtn) { + const hasLoyaltyItem = cart.some(item => item.is_loyalty); + const hasNonLoyaltyItem = cart.some(item => !item.is_loyalty); + + redeemLoyaltyBtn.disabled = !currentCustomer.eligible_for_free_meal || !hasLoyaltyItem || hasNonLoyaltyItem; + + if (loyaltyMessage) { + if (currentCustomer.eligible_for_free_meal) { + const count = currentCustomer.eligible_count || 1; + loyaltyMessage.innerHTML = `Eligible for ${count} Free Product${count > 1 ? 's' : ''}!`; + + if (hasNonLoyaltyItem) { + loyaltyMessage.innerHTML += '
(Remove non-loyalty products to redeem)
'; + } else if (!hasLoyaltyItem) { + loyaltyMessage.innerHTML += '
(Add a loyalty product to redeem)
'; + } + } else { + loyaltyMessage.textContent = `${currentCustomer.points_needed || 0} pts away from a free product.`; + } + } + } + } else { + loyaltySection.classList.add('d-none'); + } + } + function selectCustomer(cust) { currentCustomer = cust; if (selectedCustomerId) selectedCustomerId.value = cust.id; @@ -262,18 +295,7 @@ document.addEventListener('DOMContentLoaded', () => { if (customerResults) customerResults.style.display = 'none'; if (clearCustomerBtn) clearCustomerBtn.classList.remove('d-none'); - if (loyaltySection && typeof LOYALTY_SETTINGS !== 'undefined' && LOYALTY_SETTINGS.is_enabled) { - loyaltySection.classList.remove('d-none'); - if (loyaltyPointsDisplay) loyaltyPointsDisplay.textContent = cust.points + ' pts'; - if (redeemLoyaltyBtn) { - const hasLoyaltyItem = cart.some(item => item.is_loyalty); - redeemLoyaltyBtn.disabled = !cust.eligible_for_free_meal || !hasLoyaltyItem; - if (loyaltyMessage) { - if (cust.eligible_for_free_meal) loyaltyMessage.innerHTML = 'Eligible for Free Meal!'; - else loyaltyMessage.textContent = `${cust.points_needed || 0} pts away from a free meal.`; - } - } - } + updateLoyaltyUI(); isLoyaltyRedemption = false; } @@ -287,7 +309,7 @@ document.addEventListener('DOMContentLoaded', () => { } clearCustomerBtn.classList.add('d-none'); if (customerInfo) customerInfo.classList.add('d-none'); - if (loyaltySection) loyaltySection.classList.add('d-none'); + updateLoyaltyUI(); isLoyaltyRedemption = false; updateCart(); }); @@ -539,8 +561,10 @@ document.addEventListener('DOMContentLoaded', () => { function updateCart() { if (!cartItemsContainer) return; + + updateLoyaltyUI(); + if (cart.length === 0) { - if (redeemLoyaltyBtn) redeemLoyaltyBtn.disabled = true; cartItemsContainer.innerHTML = `

${_t('cart_empty')}

`; if (cartSubtotal) cartSubtotal.innerText = formatCurrency(0); if (cartTotalPrice) cartTotalPrice.innerText = formatCurrency(0); diff --git a/assets/js/main.js?offset=254&limit=33 b/assets/js/main.js?offset=254&limit=33 new file mode 100644 index 0000000..fa7a7e6 --- /dev/null +++ b/assets/js/main.js?offset=254&limit=33 @@ -0,0 +1,33 @@ + + function updateLoyaltyUI() { + if (!loyaltySection || typeof LOYALTY_SETTINGS === 'undefined' || !LOYALTY_SETTINGS.is_enabled) return; + + if (currentCustomer) { + loyaltySection.classList.remove('d-none'); + if (loyaltyPointsDisplay) loyaltyPointsDisplay.textContent = currentCustomer.points + ' pts'; + + if (redeemLoyaltyBtn) { + const hasLoyaltyItem = cart.some(item => item.is_loyalty); + const hasNonLoyaltyItem = cart.some(item => !item.is_loyalty); + + redeemLoyaltyBtn.disabled = !currentCustomer.eligible_for_free_meal || !hasLoyaltyItem || hasNonLoyaltyItem; + + if (loyaltyMessage) { + if (currentCustomer.eligible_for_free_meal) { + const count = currentCustomer.eligible_count || 1; + loyaltyMessage.innerHTML = `Eligible for ${count} Free Product${count > 1 ? 's' : ''}!`; + + if (hasNonLoyaltyItem) { + loyaltyMessage.innerHTML += '
(Remove non-loyalty products to redeem)
'; + } else if (!hasLoyaltyItem) { + loyaltyMessage.innerHTML += '
(Add a loyalty product to redeem)
'; + } + } else { + loyaltyMessage.textContent = `${currentCustomer.points_needed || 0} pts away from a free product.`; + } + } + } + } else { + loyaltySection.classList.add('d-none'); + } + }