change loyalty system
This commit is contained in:
parent
9eaaf40d0f
commit
2bb3386b5d
@ -72,11 +72,11 @@ include 'includes/header.php';
|
||||
<h6 class="card-subtitle mb-2 text-muted text-uppercase small">Current Configuration</h6>
|
||||
<div class="d-flex justify-content-between align-items-center mt-3">
|
||||
<div>
|
||||
<span class="d-block text-muted small">Points per Order</span>
|
||||
<span class="d-block text-muted small">Points per Loyalty Product</span>
|
||||
<span class="fs-4 fw-bold text-primary"><?= $settings['points_per_order'] ?> pts</span>
|
||||
</div>
|
||||
<div class="border-start ps-3">
|
||||
<span class="d-block text-muted small">Free Meal Threshold</span>
|
||||
<span class="d-block text-muted small">Free Product Threshold</span>
|
||||
<span class="fs-4 fw-bold text-success"><?= $settings['points_for_free_meal'] ?> pts</span>
|
||||
</div>
|
||||
</div>
|
||||
@ -127,7 +127,7 @@ include 'includes/header.php';
|
||||
<td>
|
||||
<?php if ($eligible): ?>
|
||||
<span class="badge bg-success-subtle text-success border border-success-subtle">
|
||||
<i class="bi bi-gift-fill me-1"></i> Eligible for Free Meal
|
||||
<i class="bi bi-gift-fill me-1"></i> Eligible for Free Product
|
||||
</span>
|
||||
<?php else: ?>
|
||||
<span class="text-muted small">
|
||||
@ -190,14 +190,14 @@ include 'includes/header.php';
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Points per Order</label>
|
||||
<label class="form-label">Points per Loyalty Product</label>
|
||||
<input type="number" name="points_per_order" class="form-control" value="<?= $settings['points_per_order'] ?>" min="0" required>
|
||||
<div class="form-text">Points awarded for every completed order.</div>
|
||||
<div class="form-text">Points awarded for every loyalty-participating product in the order.</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Points for Free Meal</label>
|
||||
<label class="form-label">Points for Free Product</label>
|
||||
<input type="number" name="points_for_free_meal" class="form-control" value="<?= $settings['points_for_free_meal'] ?>" min="0" required>
|
||||
<div class="form-text">Threshold points required to redeem a free meal.</div>
|
||||
<div class="form-text">Threshold points required to redeem a free product.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
|
||||
@ -47,16 +47,16 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
|
||||
if (!has_permission('products_edit')) {
|
||||
$message = '<div class="alert alert-danger">Access Denied: You do not have permission to edit products.</div>';
|
||||
} 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]);
|
||||
$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 = ?, is_loyalty = ? 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, $is_loyalty, $id]);
|
||||
$message = '<div class="alert alert-success">Product updated successfully!</div>';
|
||||
}
|
||||
} elseif ($action === 'add_product') {
|
||||
if (!has_permission('products_add')) {
|
||||
$message = '<div class="alert alert-danger">Access Denied: You do not have permission to add products.</div>';
|
||||
} 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]);
|
||||
$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, is_loyalty) 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, $is_loyalty]);
|
||||
$message = '<div class="alert alert-success">Product created successfully!</div>';
|
||||
}
|
||||
}
|
||||
@ -339,6 +339,15 @@ include 'includes/header.php';
|
||||
</div>
|
||||
|
||||
<div class="col-12 mt-4">
|
||||
<div class="col-12 mt-2">
|
||||
<div class="form-check form-switch p-3 bg-light rounded-3 border d-flex justify-content-between align-items-center">
|
||||
<div class="ms-1">
|
||||
<label class="form-check-label fw-bold text-dark mb-0" for="productIsLoyalty">Loyalty System Participation</label>
|
||||
<div class="small text-muted">Include this product in earning and redeeming loyalty points</div>
|
||||
</div>
|
||||
<input class="form-check-input ms-0" type="checkbox" name="is_loyalty" id="productIsLoyalty" style="width: 2.5rem; height: 1.25rem;">
|
||||
</div>
|
||||
</div>
|
||||
<h6 class="fw-bold border-bottom pb-2 mb-3"><i class="bi bi-percent me-1"></i> Promotion Settings</h6>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-4">
|
||||
|
||||
134
api/order.php
134
api/order.php
@ -74,7 +74,6 @@ try {
|
||||
$settingsStmt = $pdo->query("SELECT is_enabled, points_per_order, points_for_free_meal FROM loyalty_settings WHERE id = 1");
|
||||
$loyaltySettings = $settingsStmt->fetch(PDO::FETCH_ASSOC);
|
||||
$loyalty_enabled = $loyaltySettings ? (bool)$loyaltySettings['is_enabled'] : true;
|
||||
$points_per_order = $loyaltySettings ? intval($loyaltySettings['points_per_order']) : 10;
|
||||
$points_threshold = $loyaltySettings ? intval($loyaltySettings['points_for_free_meal']) : 70;
|
||||
|
||||
$current_points = 0;
|
||||
@ -96,58 +95,21 @@ try {
|
||||
}
|
||||
}
|
||||
|
||||
// Loyalty Redemption Logic
|
||||
// Loyalty Redemption Logic (initial check)
|
||||
$redeem_loyalty = !empty($data['redeem_loyalty']) && $loyalty_enabled;
|
||||
if ($redeem_loyalty && $customer_id) {
|
||||
if ($current_points < $points_threshold) {
|
||||
throw new Exception("Insufficient loyalty points for redemption.");
|
||||
}
|
||||
// Deduct points
|
||||
$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)
|
||||
$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");
|
||||
$ptStmt->execute();
|
||||
$loyaltyPt = $ptStmt->fetchColumn();
|
||||
if ($loyaltyPt) {
|
||||
$data['payment_type_id'] = $loyaltyPt;
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
$user = get_logged_user();
|
||||
$user_id = $user ? $user['id'] : null;
|
||||
$payment_type_id = !empty($data['payment_type_id']) ? intval($data['payment_type_id']) : null;
|
||||
|
||||
// VAT vs Discount: We repurpose the 'discount' column in the database to store VAT value.
|
||||
// If it's a positive value, it's VAT. If negative, it acts as a discount (loyalty).
|
||||
$vat = isset($data['vat']) ? floatval($data['vat']) : 0.00;
|
||||
|
||||
// Total amount will be recalculated on server to be safe
|
||||
$calculated_total = 0;
|
||||
|
||||
// First, process items to calculate real total
|
||||
// First, process items to calculate real total and handle loyalty
|
||||
$processed_items = [];
|
||||
$loyalty_items_indices = [];
|
||||
|
||||
if (!empty($data['items']) && is_array($data['items'])) {
|
||||
foreach ($data['items'] as $item) {
|
||||
$pid = $item['product_id'] ?? ($item['id'] ?? null);
|
||||
@ -176,8 +138,9 @@ try {
|
||||
}
|
||||
}
|
||||
|
||||
$item_total = $unit_price * $qty;
|
||||
$calculated_total += $item_total;
|
||||
if ($product['is_loyalty']) {
|
||||
$loyalty_items_indices[] = count($processed_items);
|
||||
}
|
||||
|
||||
$processed_items[] = [
|
||||
'product_id' => $pid,
|
||||
@ -185,13 +148,91 @@ try {
|
||||
'variant_id' => $vid,
|
||||
'variant_name' => $variant_name,
|
||||
'quantity' => $qty,
|
||||
'unit_price' => $unit_price
|
||||
'unit_price' => $unit_price,
|
||||
'is_loyalty' => (bool)$product['is_loyalty']
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Handle Loyalty Redemption (Make one loyalty item free)
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
// 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();
|
||||
|
||||
// Make ONE unit of the most expensive loyalty product free
|
||||
if ($processed_items[$max_index]['quantity'] > 1) {
|
||||
$processed_items[$max_index]['quantity']--;
|
||||
$free_item = $processed_items[$max_index];
|
||||
$free_item['quantity'] = 1;
|
||||
$free_item['unit_price'] = 0;
|
||||
$processed_items[] = $free_item;
|
||||
} else {
|
||||
$processed_items[$max_index]['unit_price'] = 0;
|
||||
}
|
||||
|
||||
// --- OVERRIDE PAYMENT TYPE ---
|
||||
$ptStmt = $pdo->prepare("SELECT id FROM payment_types WHERE name = 'Loyalty Redeem' LIMIT 1");
|
||||
$ptStmt->execute();
|
||||
$loyaltyPt = $ptStmt->fetchColumn();
|
||||
if ($loyaltyPt) {
|
||||
$data['payment_type_id'] = $loyaltyPt;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Recalculate Subtotal and Earned Points
|
||||
foreach ($processed_items as $pi) {
|
||||
$calculated_total += $pi['unit_price'] * $pi['quantity'];
|
||||
// Award points for PAID loyalty items (10 points each)
|
||||
if ($pi['is_loyalty'] && $pi['unit_price'] > 0) {
|
||||
$points_awarded += $pi['quantity'] * 10;
|
||||
}
|
||||
}
|
||||
|
||||
// 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.
|
||||
if ($customer_id && $loyalty_enabled && $points_awarded > 0) {
|
||||
$awardStmt = $pdo->prepare("UPDATE customers SET points = points + ? WHERE id = ?");
|
||||
$awardStmt->execute([$points_awarded, $customer_id]);
|
||||
|
||||
// Record Loyalty History (Award)
|
||||
$historyStmt = $pdo->prepare("INSERT INTO loyalty_points_history (customer_id, points_change, reason) VALUES (?, ?, 'Earned from Products')");
|
||||
$historyStmt->execute([$customer_id, $points_awarded]);
|
||||
$award_history_id = $pdo->lastInsertId();
|
||||
}
|
||||
|
||||
$vat = isset($data['vat']) ? floatval($data['vat']) : 0.00;
|
||||
$final_total = max(0, $calculated_total + $vat);
|
||||
|
||||
// User/Payment info
|
||||
$user = get_logged_user();
|
||||
$user_id = $user ? $user['id'] : null;
|
||||
$payment_type_id = !empty($data['payment_type_id']) ? intval($data['payment_type_id']) : null;
|
||||
|
||||
// Commission Calculation
|
||||
$commission_amount = 0;
|
||||
$companySettings = get_company_settings();
|
||||
@ -200,7 +241,6 @@ try {
|
||||
$userStmt->execute([$user_id]);
|
||||
$commission_rate = (float)$userStmt->fetchColumn();
|
||||
if ($commission_rate > 0) {
|
||||
// Commission is usually calculated on the subtotal (before tax/VAT)
|
||||
$commission_amount = $calculated_total * ($commission_rate / 100);
|
||||
}
|
||||
}
|
||||
@ -220,7 +260,6 @@ try {
|
||||
}
|
||||
|
||||
if ($is_update) {
|
||||
// UPDATE Existing Order
|
||||
$stmt = $pdo->prepare("UPDATE orders SET
|
||||
outlet_id = ?, table_id = ?, table_number = ?, order_type = ?,
|
||||
customer_id = ?, customer_name = ?, customer_phone = ?,
|
||||
@ -237,7 +276,6 @@ try {
|
||||
$delStmt = $pdo->prepare("DELETE FROM order_items WHERE order_id = ?");
|
||||
$delStmt->execute([$order_id]);
|
||||
} else {
|
||||
// INSERT New Order
|
||||
$stmt = $pdo->prepare("INSERT INTO orders (outlet_id, table_id, table_number, order_type, customer_id, customer_name, customer_phone, payment_type_id, total_amount, discount, user_id, commission_amount, status) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'pending')");
|
||||
$stmt->execute([$outlet_id, $table_id, $table_number, $order_type, $customer_id, $customer_name, $customer_phone, $payment_type_id, $final_total, $vat, $user_id, $commission_amount]);
|
||||
$order_id = $pdo->lastInsertId();
|
||||
|
||||
@ -266,7 +266,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
loyaltySection.classList.remove('d-none');
|
||||
if (loyaltyPointsDisplay) loyaltyPointsDisplay.textContent = cust.points + ' pts';
|
||||
if (redeemLoyaltyBtn) {
|
||||
redeemLoyaltyBtn.disabled = !cust.eligible_for_free_meal;
|
||||
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.`;
|
||||
@ -476,7 +477,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
addToCart({
|
||||
id: product.id, name: product.name, name_ar: product.name_ar || "",
|
||||
price: parseFloat(product.price), base_price: parseFloat(product.price),
|
||||
hasVariants: false, quantity: 1, variant_id: null, variant_name: null
|
||||
hasVariants: false, quantity: 1, variant_id: null, variant_name: null, is_loyalty: parseInt(product.is_loyalty) === 1
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -498,7 +499,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
addToCart({
|
||||
id: product.id, name: product.name, name_ar: product.name_ar || "",
|
||||
price: finalPrice, base_price: parseFloat(product.price),
|
||||
hasVariants: true, quantity: 1, variant_id: v.id, variant_name: v.name
|
||||
hasVariants: true, quantity: 1, variant_id: v.id, variant_name: v.name, is_loyalty: parseInt(product.is_loyalty) === 1
|
||||
});
|
||||
variantSelectionModal.hide();
|
||||
};
|
||||
@ -539,6 +540,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
function updateCart() {
|
||||
if (!cartItemsContainer) return;
|
||||
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>`;
|
||||
if (cartSubtotal) cartSubtotal.innerText = formatCurrency(0);
|
||||
if (cartTotalPrice) cartTotalPrice.innerText = formatCurrency(0);
|
||||
|
||||
2
db/migrations/038_add_is_loyalty_to_products.sql
Normal file
2
db/migrations/038_add_is_loyalty_to_products.sql
Normal file
@ -0,0 +1,2 @@
|
||||
-- Add is_loyalty column to products table
|
||||
ALTER TABLE products ADD COLUMN is_loyalty TINYINT(1) DEFAULT 0;
|
||||
@ -18,6 +18,7 @@ CREATE TABLE IF NOT EXISTS products (
|
||||
description TEXT,
|
||||
price DECIMAL(10, 2) NOT NULL,
|
||||
image_url VARCHAR(255),
|
||||
is_loyalty TINYINT(1) DEFAULT 0,
|
||||
FOREIGN KEY (category_id) REFERENCES categories(id)
|
||||
);
|
||||
|
||||
|
||||
2
pos.php
2
pos.php
@ -310,7 +310,7 @@ if (!$loyalty_settings) {
|
||||
</div>
|
||||
</div>
|
||||
<div id="loyalty-message" class="small text-muted mb-2" style="font-size: 0.75rem;"></div>
|
||||
<button type="button" class="btn btn-primary btn-sm w-100 fw-bold rounded-3" id="redeem-loyalty-btn" disabled>Redeem Free Meal</button>
|
||||
<button type="button" class="btn btn-primary btn-sm w-100 fw-bold rounded-3" id="redeem-loyalty-btn" disabled>Redeem Free Product</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user