loyalty update
This commit is contained in:
parent
a1d6822c0a
commit
600e86d0fa
@ -141,6 +141,8 @@ try {
|
|||||||
|
|
||||||
if ($product['is_loyalty']) {
|
if ($product['is_loyalty']) {
|
||||||
$loyalty_items_indices[] = count($processed_items);
|
$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[] = [
|
$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 ($redeem_loyalty && $customer_id) {
|
||||||
if (empty($loyalty_items_indices)) {
|
if (empty($loyalty_items_indices)) {
|
||||||
throw new Exception("No loyalty-eligible products in the order to redeem.");
|
throw new Exception("No loyalty-eligible products in the order to redeem.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find the most expensive loyalty item
|
$possible_redemptions = floor($current_points / $points_threshold);
|
||||||
$max_price = -1;
|
$redemptions_done = 0;
|
||||||
$max_index = -1;
|
|
||||||
foreach ($loyalty_items_indices as $idx) {
|
while ($redemptions_done < $possible_redemptions) {
|
||||||
if ($processed_items[$idx]['unit_price'] > $max_price) {
|
// Find the most expensive loyalty item that is still paid
|
||||||
$max_price = $processed_items[$idx]['unit_price'];
|
$max_price = -1;
|
||||||
$max_index = $idx;
|
$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) {
|
if ($max_index === -1) break; // No more loyalty items to redeem
|
||||||
// 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;
|
|
||||||
|
|
||||||
// Record Loyalty History (Deduction)
|
// Deduct points for ONE redemption
|
||||||
$historyStmt = $pdo->prepare("INSERT INTO loyalty_points_history (customer_id, points_change, reason) VALUES (?, ?, 'Redeemed Free Product')");
|
$points_deducted += $points_threshold;
|
||||||
$historyStmt->execute([$customer_id, -$points_threshold]);
|
$redemptions_done++;
|
||||||
$redeem_history_id = $pdo->lastInsertId();
|
|
||||||
|
|
||||||
// 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) {
|
if ($processed_items[$max_index]['quantity'] > 1) {
|
||||||
$processed_items[$max_index]['quantity']--;
|
$processed_items[$max_index]['quantity']--;
|
||||||
$free_item = $processed_items[$max_index];
|
$free_item = $processed_items[$max_index];
|
||||||
@ -193,6 +195,19 @@ try {
|
|||||||
} else {
|
} else {
|
||||||
$processed_items[$max_index]['unit_price'] = 0;
|
$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 ---
|
// --- OVERRIDE PAYMENT TYPE ---
|
||||||
$ptStmt = $pdo->prepare("SELECT id FROM payment_types WHERE name = 'Loyalty Redeem' LIMIT 1");
|
$ptStmt = $pdo->prepare("SELECT id FROM payment_types WHERE name = 'Loyalty Redeem' LIMIT 1");
|
||||||
@ -201,6 +216,8 @@ try {
|
|||||||
if ($loyaltyPt) {
|
if ($loyaltyPt) {
|
||||||
$data['payment_type_id'] = $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) {
|
foreach ($processed_items as $pi) {
|
||||||
$calculated_total += $pi['unit_price'] * $pi['quantity'];
|
$calculated_total += $pi['unit_price'] * $pi['quantity'];
|
||||||
// Award points for PAID loyalty items
|
// 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;
|
$points_awarded += $pi['quantity'] * $points_per_product;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Award Points (ONLY IF NOT REDEEMING or we can award for remaining items)
|
// Award Points
|
||||||
// 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.
|
|
||||||
if ($customer_id && $loyalty_enabled && $points_awarded > 0) {
|
if ($customer_id && $loyalty_enabled && $points_awarded > 0) {
|
||||||
$awardStmt = $pdo->prepare("UPDATE customers SET points = points + ? WHERE id = ?");
|
$awardStmt = $pdo->prepare("UPDATE customers SET points = points + ? WHERE id = ?");
|
||||||
$awardStmt->execute([$points_awarded, $customer_id]);
|
$awardStmt->execute([$points_awarded, $customer_id]);
|
||||||
|
|||||||
@ -47,9 +47,9 @@ try {
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch Items
|
// Fetch Items with is_loyalty
|
||||||
$stmtItems = $pdo->prepare("
|
$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
|
FROM order_items oi
|
||||||
JOIN products p ON oi.product_id = p.id
|
JOIN products p ON oi.product_id = p.id
|
||||||
LEFT JOIN product_variants v ON oi.variant_id = v.id
|
LEFT JOIN product_variants v ON oi.variant_id = v.id
|
||||||
@ -65,13 +65,12 @@ try {
|
|||||||
'id' => $item['product_id'],
|
'id' => $item['product_id'],
|
||||||
'name' => $item['product_name'],
|
'name' => $item['product_name'],
|
||||||
'price' => floatval($item['unit_price']),
|
'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?
|
'base_price' => floatval($item['base_price']),
|
||||||
// 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.
|
|
||||||
'quantity' => intval($item['quantity']),
|
'quantity' => intval($item['quantity']),
|
||||||
'variant_id' => $item['variant_id'],
|
'variant_id' => $item['variant_id'],
|
||||||
'variant_name' => $item['variant_name'],
|
'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 = $pdo->prepare("SELECT * FROM customers WHERE id = ?");
|
||||||
$cStmt->execute([$order['customer_id']]);
|
$cStmt->execute([$order['customer_id']]);
|
||||||
$customer = $cStmt->fetch(PDO::FETCH_ASSOC);
|
$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([
|
echo json_encode([
|
||||||
|
|||||||
@ -15,7 +15,7 @@ try {
|
|||||||
// Fetch Loyalty Settings
|
// Fetch Loyalty Settings
|
||||||
$settingsStmt = $pdo->query("SELECT points_for_free_meal FROM loyalty_settings WHERE id = 1");
|
$settingsStmt = $pdo->query("SELECT points_for_free_meal FROM loyalty_settings WHERE id = 1");
|
||||||
$settings = $settingsStmt->fetch(PDO::FETCH_ASSOC);
|
$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");
|
$stmt = $pdo->prepare("SELECT id, name, phone, email, points FROM customers WHERE name LIKE ? OR phone LIKE ? LIMIT 10");
|
||||||
$searchTerm = "%$q%";
|
$searchTerm = "%$q%";
|
||||||
@ -25,7 +25,8 @@ try {
|
|||||||
foreach ($customers as &$customer) {
|
foreach ($customers as &$customer) {
|
||||||
$customer['points'] = intval($customer['points']);
|
$customer['points'] = intval($customer['points']);
|
||||||
$customer['eligible_for_free_meal'] = $customer['points'] >= $threshold;
|
$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;
|
$customer['threshold'] = $threshold;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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 = `<span class="text-success fw-bold">Eligible for ${count} Free Product${count > 1 ? 's' : ''}!</span>`;
|
||||||
|
|
||||||
|
if (hasNonLoyaltyItem) {
|
||||||
|
loyaltyMessage.innerHTML += '<div class="text-danger small" style="font-size: 0.7rem;">(Remove non-loyalty products to redeem)</div>';
|
||||||
|
} else if (!hasLoyaltyItem) {
|
||||||
|
loyaltyMessage.innerHTML += '<div class="text-warning small" style="font-size: 0.7rem;">(Add a loyalty product to redeem)</div>';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
loyaltyMessage.textContent = `${currentCustomer.points_needed || 0} pts away from a free product.`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
loyaltySection.classList.add('d-none');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function selectCustomer(cust) {
|
function selectCustomer(cust) {
|
||||||
currentCustomer = cust;
|
currentCustomer = cust;
|
||||||
if (selectedCustomerId) selectedCustomerId.value = cust.id;
|
if (selectedCustomerId) selectedCustomerId.value = cust.id;
|
||||||
@ -262,18 +295,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
if (customerResults) customerResults.style.display = 'none';
|
if (customerResults) customerResults.style.display = 'none';
|
||||||
if (clearCustomerBtn) clearCustomerBtn.classList.remove('d-none');
|
if (clearCustomerBtn) clearCustomerBtn.classList.remove('d-none');
|
||||||
|
|
||||||
if (loyaltySection && typeof LOYALTY_SETTINGS !== 'undefined' && LOYALTY_SETTINGS.is_enabled) {
|
updateLoyaltyUI();
|
||||||
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 = '<span class="text-success fw-bold">Eligible for Free Meal!</span>';
|
|
||||||
else loyaltyMessage.textContent = `${cust.points_needed || 0} pts away from a free meal.`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
isLoyaltyRedemption = false;
|
isLoyaltyRedemption = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -287,7 +309,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
}
|
}
|
||||||
clearCustomerBtn.classList.add('d-none');
|
clearCustomerBtn.classList.add('d-none');
|
||||||
if (customerInfo) customerInfo.classList.add('d-none');
|
if (customerInfo) customerInfo.classList.add('d-none');
|
||||||
if (loyaltySection) loyaltySection.classList.add('d-none');
|
updateLoyaltyUI();
|
||||||
isLoyaltyRedemption = false;
|
isLoyaltyRedemption = false;
|
||||||
updateCart();
|
updateCart();
|
||||||
});
|
});
|
||||||
@ -539,8 +561,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
|
|
||||||
function updateCart() {
|
function updateCart() {
|
||||||
if (!cartItemsContainer) return;
|
if (!cartItemsContainer) return;
|
||||||
|
|
||||||
|
updateLoyaltyUI();
|
||||||
|
|
||||||
if (cart.length === 0) {
|
if (cart.length === 0) {
|
||||||
if (redeemLoyaltyBtn) redeemLoyaltyBtn.disabled = true;
|
|
||||||
cartItemsContainer.innerHTML = `<div class="text-center text-muted mt-5"><i class="bi bi-basket3 fs-1 text-light"></i><p class="mt-2">${_t('cart_empty')}</p></div>`;
|
cartItemsContainer.innerHTML = `<div class="text-center text-muted mt-5"><i class="bi bi-basket3 fs-1 text-light"></i><p class="mt-2">${_t('cart_empty')}</p></div>`;
|
||||||
if (cartSubtotal) cartSubtotal.innerText = formatCurrency(0);
|
if (cartSubtotal) cartSubtotal.innerText = formatCurrency(0);
|
||||||
if (cartTotalPrice) cartTotalPrice.innerText = formatCurrency(0);
|
if (cartTotalPrice) cartTotalPrice.innerText = formatCurrency(0);
|
||||||
|
|||||||
33
assets/js/main.js?offset=254&limit=33
Normal file
33
assets/js/main.js?offset=254&limit=33
Normal file
@ -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 = `<span class="text-success fw-bold">Eligible for ${count} Free Product${count > 1 ? 's' : ''}!</span>`;
|
||||||
|
|
||||||
|
if (hasNonLoyaltyItem) {
|
||||||
|
loyaltyMessage.innerHTML += '<div class="text-danger small" style="font-size: 0.7rem;">(Remove non-loyalty products to redeem)</div>';
|
||||||
|
} else if (!hasLoyaltyItem) {
|
||||||
|
loyaltyMessage.innerHTML += '<div class="text-warning small" style="font-size: 0.7rem;">(Add a loyalty product to redeem)</div>';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
loyaltyMessage.textContent = `${currentCustomer.points_needed || 0} pts away from a free product.`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
loyaltySection.classList.add('d-none');
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user