Upsells and discounts improved
This commit is contained in:
parent
bf84540401
commit
9c816cb23b
31
index.html
31
index.html
@ -164,8 +164,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- BOGO Actie Banner -->
|
||||
<div x-show="activeProduct && activeProduct.bogo_rules && activeProduct.bogo_rules.length > 0" x-cloak
|
||||
<!-- BOGO Actie Banner - Only for buy_x_get_x rules -->
|
||||
<div x-show="activeProduct && activeProduct.bogo_rules && activeProduct.bogo_rules.length > 0 && activeProduct.bogo_rules[0].type === 'buy_x_get_x'" x-cloak
|
||||
class="mb-6 p-4 bg-gradient-to-r from-red-500 to-pink-500 rounded-2xl text-white shadow-lg">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<div class="flex items-center gap-3">
|
||||
@ -205,7 +205,32 @@
|
||||
Wellicht ook interessant</p>
|
||||
|
||||
<template x-for="u in recommendedOptions" :key="u.id">
|
||||
<div
|
||||
<!-- Product with BOGO discount - styled like action block -->
|
||||
<div x-show="getYProductBogoDiscount(u)"
|
||||
class="p-4 bg-gradient-to-r from-red-500 to-pink-500 rounded-2xl text-white shadow-lg">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="bg-white/20 p-2 rounded-full">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v13m0-13V6a2 2 0 112 2h-2zm0 0V5.5A2.5 2.5 0 109.5 8H12zm-7 4h14M5 12a2 2 0 110-4h14a2 2 0 110 4M5 12v7a2 2 0 002 2h10a2 2 0 002-2v-7" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-black text-sm uppercase tracking-wide" x-text="u.name"></p>
|
||||
<p class="text-[10px] opacity-80" x-text="getYProductDiscountLabel(u)"></p>
|
||||
</div>
|
||||
</div>
|
||||
<button @click="toggleUpsell(u)"
|
||||
:class="isInCart(u.id) ? 'bg-slate-700' : 'bg-white'"
|
||||
:disabled="isInCart(u.id)"
|
||||
class="text-red-500 px-4 py-2 rounded-xl font-black text-xs uppercase shadow-md hover:bg-red-50 transition disabled:opacity-50 disabled:cursor-not-allowed whitespace-nowrap">
|
||||
<span x-text="isInCart(u.id) ? 'Toegevoegd' : ('+ €' + getYProductDiscountedPrice(u))"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Regular product without BOGO discount -->
|
||||
<div x-show="!getYProductBogoDiscount(u)"
|
||||
class="flex items-center justify-between p-4 border rounded-2xl bg-slate-50 hover:bg-white transition-all shadow-sm">
|
||||
<span class="text-xs font-bold text-slate-700"
|
||||
x-text="u.name + ' (€' + u.price + ')'"></span>
|
||||
|
||||
@ -69,6 +69,25 @@ const CartComponent = {
|
||||
if (idx > -1) {
|
||||
this.cart.splice(idx, 1);
|
||||
} else {
|
||||
// Check if this product has a Y discount from active product
|
||||
const yDiscount = this.getYProductBogoDiscount(product);
|
||||
|
||||
if (yDiscount) {
|
||||
// Add with discounted price
|
||||
const discountedPrice = this.getYProductDiscountedPrice(product);
|
||||
const discountLabel = this.getYProductDiscountLabel(product);
|
||||
|
||||
this.cart.push({
|
||||
id: parseInt(product.id),
|
||||
name: product.name + (discountLabel ? ' (' + discountLabel + ')' : ''),
|
||||
price: discountedPrice,
|
||||
originalPrice: product.price,
|
||||
isFree: false,
|
||||
isDiscounted: true,
|
||||
ruleId: yDiscount.rule_id
|
||||
});
|
||||
} else {
|
||||
// Add with regular price
|
||||
this.cart.push({
|
||||
id: parseInt(product.id),
|
||||
name: product.name,
|
||||
@ -76,6 +95,7 @@ const CartComponent = {
|
||||
isFree: false
|
||||
});
|
||||
}
|
||||
}
|
||||
this.recalculateBogo();
|
||||
},
|
||||
|
||||
@ -83,7 +103,7 @@ const CartComponent = {
|
||||
* Recalculate BOGO discounts based on current cart
|
||||
*/
|
||||
recalculateBogo() {
|
||||
// Remove existing free items from cart
|
||||
// Remove existing auto-added free items from cart (but keep manually added discounted items)
|
||||
this.cart = this.cart.filter(item => !item.isFree);
|
||||
|
||||
// Calculate new BOGO
|
||||
@ -93,7 +113,8 @@ const CartComponent = {
|
||||
this.bogoFreeItems = bogoResult.freeItems;
|
||||
this.appliedBogoRules = bogoResult.appliedRules;
|
||||
|
||||
// Add free items to cart
|
||||
// Add free items to cart (only for buy_x_get_x rules)
|
||||
// buy_x_get_y items are NOT auto-added, user must add them manually
|
||||
for (const freeItem of bogoResult.freeItems) {
|
||||
this.cart.push({
|
||||
id: parseInt(freeItem.id),
|
||||
@ -240,6 +261,59 @@ const CartComponent = {
|
||||
return BogoService.hasBogoRules(product);
|
||||
},
|
||||
|
||||
/**
|
||||
* Get BOGO discount info for a Y product (upsell) based on active product's rules
|
||||
* @param {Object} yProduct - The upsell product
|
||||
* @returns {Object|null}
|
||||
*/
|
||||
getYProductBogoDiscount(yProduct) {
|
||||
if (!this.activeProduct) return null;
|
||||
return BogoService.getYProductDiscount(this.activeProduct, yProduct);
|
||||
},
|
||||
|
||||
/**
|
||||
* Get discounted price for Y product
|
||||
* @param {Object} yProduct - The upsell product
|
||||
* @returns {string|null}
|
||||
*/
|
||||
getYProductDiscountedPrice(yProduct) {
|
||||
const discount = this.getYProductBogoDiscount(yProduct);
|
||||
if (!discount) return null;
|
||||
|
||||
const price = parseFloat(yProduct.price);
|
||||
|
||||
if (discount.discount_type === 'free_product' || discount.discount_value >= 100) {
|
||||
return '0.00';
|
||||
} else if (discount.discount_type === 'percentage') {
|
||||
const discountAmount = price * (discount.discount_value / 100);
|
||||
return (price - discountAmount).toFixed(2);
|
||||
} else if (discount.discount_type === 'flat') {
|
||||
return Math.max(0, price - discount.discount_value).toFixed(2);
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get discount label for Y product
|
||||
* @param {Object} yProduct - The upsell product
|
||||
* @returns {string|null}
|
||||
*/
|
||||
getYProductDiscountLabel(yProduct) {
|
||||
const discount = this.getYProductBogoDiscount(yProduct);
|
||||
if (!discount) return null;
|
||||
|
||||
if (discount.discount_type === 'free_product' || discount.discount_value >= 100) {
|
||||
return 'GRATIS';
|
||||
} else if (discount.discount_type === 'percentage') {
|
||||
return discount.discount_value + '% korting';
|
||||
} else if (discount.discount_type === 'flat') {
|
||||
return '€' + discount.discount_value.toFixed(2) + ' korting';
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Clear the cart
|
||||
*/
|
||||
@ -270,27 +344,24 @@ const CartComponent = {
|
||||
getComputed() {
|
||||
return {
|
||||
/**
|
||||
* Calculate subtotal (items only, no shipping, no BOGO discount)
|
||||
* Calculate subtotal (items only, no shipping)
|
||||
* Includes all items: full price, discounted (isFree with price > 0), and free (price = 0)
|
||||
* @returns {string}
|
||||
*/
|
||||
subtotal() {
|
||||
return this.cart
|
||||
.filter(item => !item.isFree)
|
||||
.reduce((sum, item) => sum + parseFloat(item.price), 0)
|
||||
.toFixed(2);
|
||||
},
|
||||
|
||||
/**
|
||||
* Calculate total including shipping and BOGO discount
|
||||
* Calculate total including shipping
|
||||
* @returns {string}
|
||||
*/
|
||||
total() {
|
||||
const itemsTotal = this.cart
|
||||
.filter(item => !item.isFree)
|
||||
.reduce((sum, item) => sum + parseFloat(item.price), 0);
|
||||
const shipping = parseFloat(this.shipping) || 0;
|
||||
// Note: BOGO discount is already applied via free items with price 0
|
||||
// So we don't subtract bogoDiscount here
|
||||
return (itemsTotal + shipping).toFixed(2);
|
||||
},
|
||||
|
||||
|
||||
@ -137,21 +137,28 @@ const BogoService = {
|
||||
// These are handled manually via addBogoDiscountedItem() in cart.js
|
||||
// The user clicks a button to add the discounted item
|
||||
} else if (rule.type === 'buy_x_get_y' && rule.get_product_ids) {
|
||||
// Different product is free
|
||||
// Different product (Y) is free or discounted
|
||||
// For buy_x_get_y, we DON'T automatically add items to cart
|
||||
// The user must manually add them via the upsell section
|
||||
// We only track the discount amount for display purposes
|
||||
for (const freeProductId of rule.get_product_ids) {
|
||||
const freeProduct = products.find(p => p.id == freeProductId);
|
||||
if (!freeProduct) continue;
|
||||
|
||||
const freeProductPrice = parseFloat(freeProduct.price);
|
||||
|
||||
if (rule.discount_type === 'free_product' || rule.discount_value >= 100) {
|
||||
result.discountAmount += freeQty * parseFloat(freeProduct.price);
|
||||
result.freeItems.push({
|
||||
id: freeProduct.id,
|
||||
name: freeProduct.name + ' (GRATIS)',
|
||||
price: '0.00',
|
||||
originalPrice: freeProduct.price,
|
||||
isFree: true,
|
||||
ruleId: rule.rule_id
|
||||
});
|
||||
// 100% korting - product Y is gratis (but not auto-added)
|
||||
result.discountAmount += freeQty * freeProductPrice;
|
||||
} else if (rule.discount_type === 'percentage') {
|
||||
// Percentage korting op product Y (but not auto-added)
|
||||
const discountPerItem = freeProductPrice * (rule.discount_value / 100);
|
||||
result.discountAmount += freeQty * discountPerItem;
|
||||
} else if (rule.discount_type === 'flat') {
|
||||
// Vaste korting op product Y (but not auto-added)
|
||||
const discountedPrice = Math.max(0, freeProductPrice - rule.discount_value).toFixed(2);
|
||||
const actualDiscount = freeProductPrice - parseFloat(discountedPrice);
|
||||
result.discountAmount += freeQty * actualDiscount;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -185,5 +192,35 @@ const BogoService = {
|
||||
}
|
||||
|
||||
return suggestions;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get BOGO discount info for a Y product based on X product's rules
|
||||
*
|
||||
* @param {Object} xProduct - The main product (X) with BOGO rules
|
||||
* @param {Object} yProduct - The upsell product (Y) to check
|
||||
* @returns {Object|null} - Discount info or null if no discount applies
|
||||
*/
|
||||
getYProductDiscount(xProduct, yProduct) {
|
||||
if (!xProduct || !this.hasBogoRules(xProduct)) return null;
|
||||
|
||||
const rule = this.getPrimaryBogoRule(xProduct);
|
||||
|
||||
// Only for buy_x_get_y rules
|
||||
if (rule.type !== 'buy_x_get_y') return null;
|
||||
|
||||
// Check if yProduct is in the get_product_ids
|
||||
if (!rule.get_product_ids || !rule.get_product_ids.includes(parseInt(yProduct.id))) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Return discount info
|
||||
return {
|
||||
rule_id: rule.rule_id,
|
||||
discount_type: rule.discount_type,
|
||||
discount_value: rule.discount_value,
|
||||
label: rule.label,
|
||||
badge_text: rule.badge_text
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user