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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- BOGO Actie Banner -->
|
<!-- BOGO Actie Banner - Only for buy_x_get_x rules -->
|
||||||
<div x-show="activeProduct && activeProduct.bogo_rules && activeProduct.bogo_rules.length > 0" x-cloak
|
<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">
|
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 justify-between gap-3">
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
@ -205,7 +205,32 @@
|
|||||||
Wellicht ook interessant</p>
|
Wellicht ook interessant</p>
|
||||||
|
|
||||||
<template x-for="u in recommendedOptions" :key="u.id">
|
<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">
|
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"
|
<span class="text-xs font-bold text-slate-700"
|
||||||
x-text="u.name + ' (€' + u.price + ')'"></span>
|
x-text="u.name + ' (€' + u.price + ')'"></span>
|
||||||
|
|||||||
@ -69,6 +69,25 @@ const CartComponent = {
|
|||||||
if (idx > -1) {
|
if (idx > -1) {
|
||||||
this.cart.splice(idx, 1);
|
this.cart.splice(idx, 1);
|
||||||
} else {
|
} 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({
|
this.cart.push({
|
||||||
id: parseInt(product.id),
|
id: parseInt(product.id),
|
||||||
name: product.name,
|
name: product.name,
|
||||||
@ -76,6 +95,7 @@ const CartComponent = {
|
|||||||
isFree: false
|
isFree: false
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
this.recalculateBogo();
|
this.recalculateBogo();
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -83,7 +103,7 @@ const CartComponent = {
|
|||||||
* Recalculate BOGO discounts based on current cart
|
* Recalculate BOGO discounts based on current cart
|
||||||
*/
|
*/
|
||||||
recalculateBogo() {
|
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);
|
this.cart = this.cart.filter(item => !item.isFree);
|
||||||
|
|
||||||
// Calculate new BOGO
|
// Calculate new BOGO
|
||||||
@ -93,7 +113,8 @@ const CartComponent = {
|
|||||||
this.bogoFreeItems = bogoResult.freeItems;
|
this.bogoFreeItems = bogoResult.freeItems;
|
||||||
this.appliedBogoRules = bogoResult.appliedRules;
|
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) {
|
for (const freeItem of bogoResult.freeItems) {
|
||||||
this.cart.push({
|
this.cart.push({
|
||||||
id: parseInt(freeItem.id),
|
id: parseInt(freeItem.id),
|
||||||
@ -240,6 +261,59 @@ const CartComponent = {
|
|||||||
return BogoService.hasBogoRules(product);
|
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
|
* Clear the cart
|
||||||
*/
|
*/
|
||||||
@ -270,27 +344,24 @@ const CartComponent = {
|
|||||||
getComputed() {
|
getComputed() {
|
||||||
return {
|
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}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
subtotal() {
|
subtotal() {
|
||||||
return this.cart
|
return this.cart
|
||||||
.filter(item => !item.isFree)
|
|
||||||
.reduce((sum, item) => sum + parseFloat(item.price), 0)
|
.reduce((sum, item) => sum + parseFloat(item.price), 0)
|
||||||
.toFixed(2);
|
.toFixed(2);
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculate total including shipping and BOGO discount
|
* Calculate total including shipping
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
total() {
|
total() {
|
||||||
const itemsTotal = this.cart
|
const itemsTotal = this.cart
|
||||||
.filter(item => !item.isFree)
|
|
||||||
.reduce((sum, item) => sum + parseFloat(item.price), 0);
|
.reduce((sum, item) => sum + parseFloat(item.price), 0);
|
||||||
const shipping = parseFloat(this.shipping) || 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);
|
return (itemsTotal + shipping).toFixed(2);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@ -137,21 +137,28 @@ const BogoService = {
|
|||||||
// These are handled manually via addBogoDiscountedItem() in cart.js
|
// These are handled manually via addBogoDiscountedItem() in cart.js
|
||||||
// The user clicks a button to add the discounted item
|
// The user clicks a button to add the discounted item
|
||||||
} else if (rule.type === 'buy_x_get_y' && rule.get_product_ids) {
|
} 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) {
|
for (const freeProductId of rule.get_product_ids) {
|
||||||
const freeProduct = products.find(p => p.id == freeProductId);
|
const freeProduct = products.find(p => p.id == freeProductId);
|
||||||
if (!freeProduct) continue;
|
if (!freeProduct) continue;
|
||||||
|
|
||||||
|
const freeProductPrice = parseFloat(freeProduct.price);
|
||||||
|
|
||||||
if (rule.discount_type === 'free_product' || rule.discount_value >= 100) {
|
if (rule.discount_type === 'free_product' || rule.discount_value >= 100) {
|
||||||
result.discountAmount += freeQty * parseFloat(freeProduct.price);
|
// 100% korting - product Y is gratis (but not auto-added)
|
||||||
result.freeItems.push({
|
result.discountAmount += freeQty * freeProductPrice;
|
||||||
id: freeProduct.id,
|
} else if (rule.discount_type === 'percentage') {
|
||||||
name: freeProduct.name + ' (GRATIS)',
|
// Percentage korting op product Y (but not auto-added)
|
||||||
price: '0.00',
|
const discountPerItem = freeProductPrice * (rule.discount_value / 100);
|
||||||
originalPrice: freeProduct.price,
|
result.discountAmount += freeQty * discountPerItem;
|
||||||
isFree: true,
|
} else if (rule.discount_type === 'flat') {
|
||||||
ruleId: rule.rule_id
|
// 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;
|
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