422 lines
16 KiB
JavaScript
422 lines
16 KiB
JavaScript
/**
|
|
* Cart Component - Handles shopping cart functionality with BOGO support
|
|
*/
|
|
const CartComponent = {
|
|
/**
|
|
* Initialize cart state
|
|
* @returns {Object}
|
|
*/
|
|
getInitialState() {
|
|
return {
|
|
cart: [],
|
|
shipping: '8.95',
|
|
bogoDiscount: 0,
|
|
bogoFreeItems: [],
|
|
appliedBogoRules: []
|
|
};
|
|
},
|
|
|
|
/**
|
|
* Get cart methods for Alpine.js component
|
|
* @returns {Object}
|
|
*/
|
|
getMethods() {
|
|
return {
|
|
/**
|
|
* Add item to cart and recalculate BOGO
|
|
* @param {Object} item - Item with id, name, price, and optional variation_id
|
|
*/
|
|
addToCart(item) {
|
|
this.cart.push({
|
|
id: parseInt(item.id),
|
|
variation_id: item.variation_id || 0,
|
|
name: item.name,
|
|
price: item.price,
|
|
isFree: item.isFree || false
|
|
});
|
|
this.recalculateBogo();
|
|
},
|
|
|
|
/**
|
|
* Remove item from cart by index and recalculate BOGO
|
|
* @param {number} index
|
|
*/
|
|
removeFromCart(index) {
|
|
const item = this.cart[index];
|
|
this.cart.splice(index, 1);
|
|
|
|
// If removing a paid item, recalculate BOGO
|
|
if (!item.isFree) {
|
|
this.recalculateBogo();
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Check if product is in cart (excluding free items)
|
|
* @param {number} id
|
|
* @returns {boolean}
|
|
*/
|
|
isInCart(id) {
|
|
return this.cart.some(i => parseInt(i.id) === parseInt(id) && !i.isFree);
|
|
},
|
|
|
|
/**
|
|
* Toggle upsell product in cart
|
|
* @param {Object} product
|
|
*/
|
|
toggleUpsell(product) {
|
|
const idx = this.cart.findIndex(i => parseInt(i.id) === parseInt(product.id) && !i.isFree);
|
|
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) {
|
|
// Check if we can add more discounted items (respect get_qty limit)
|
|
if (!this.canAddYProductDiscount(product, yDiscount)) {
|
|
alert('Maximum aantal kortingsproducten bereikt voor deze actie.');
|
|
return;
|
|
}
|
|
|
|
// 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,
|
|
price: product.price,
|
|
isFree: false
|
|
});
|
|
}
|
|
}
|
|
this.recalculateBogo();
|
|
},
|
|
|
|
/**
|
|
* Recalculate BOGO discounts based on current cart
|
|
*/
|
|
recalculateBogo() {
|
|
// 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
|
|
const bogoResult = BogoService.calculateBogoForCart(this.cart, this.products);
|
|
|
|
this.bogoDiscount = bogoResult.discountAmount;
|
|
this.bogoFreeItems = bogoResult.freeItems;
|
|
this.appliedBogoRules = bogoResult.appliedRules;
|
|
|
|
// 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),
|
|
variation_id: 0,
|
|
name: freeItem.name,
|
|
price: freeItem.price,
|
|
originalPrice: freeItem.originalPrice,
|
|
isFree: true,
|
|
ruleId: freeItem.ruleId
|
|
});
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Add discounted BOGO item to cart manually
|
|
* For rules like "2e voor €19,95" where the 2nd item gets a discount
|
|
* @param {Object} product - The product with BOGO rules
|
|
*/
|
|
addBogoDiscountedItem(product) {
|
|
if (!product || !product.bogo_rules || product.bogo_rules.length === 0) {
|
|
return;
|
|
}
|
|
|
|
const rule = product.bogo_rules[0];
|
|
|
|
// Check if this is a discount type rule (not free)
|
|
if (rule.discount_type !== 'flat' && rule.discount_type !== 'percentage') {
|
|
return;
|
|
}
|
|
|
|
// Calculate discounted price
|
|
let discountedPrice;
|
|
let discountLabel;
|
|
|
|
if (rule.discount_type === 'flat') {
|
|
discountedPrice = (parseFloat(product.price) - rule.discount_value).toFixed(2);
|
|
discountLabel = '€' + rule.discount_value.toFixed(2) + ' korting';
|
|
} else if (rule.discount_type === 'percentage') {
|
|
const discount = parseFloat(product.price) * (rule.discount_value / 100);
|
|
discountedPrice = (parseFloat(product.price) - discount).toFixed(2);
|
|
discountLabel = rule.discount_value + '% korting';
|
|
}
|
|
|
|
// Add the discounted item to cart (this IS the 2nd item, not a 3rd)
|
|
// isFree: false because it's not free, just discounted
|
|
this.cart.push({
|
|
id: parseInt(product.id),
|
|
variation_id: 0,
|
|
name: product.name + ' (' + discountLabel + ')',
|
|
price: discountedPrice,
|
|
originalPrice: product.price,
|
|
isFree: false,
|
|
isDiscounted: true,
|
|
ruleId: rule.rule_id
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Check if BOGO discounted item can be added
|
|
* For "2e voor €19,95" type rules: user needs 1 item in cart, then can add the 2nd at discount
|
|
* @param {Object} product
|
|
* @returns {boolean}
|
|
*/
|
|
canAddBogoDiscountedItem(product) {
|
|
if (!product || !product.bogo_rules || product.bogo_rules.length === 0) {
|
|
return false;
|
|
}
|
|
|
|
const rule = product.bogo_rules[0];
|
|
|
|
// Only for flat or percentage discount types
|
|
if (rule.discount_type !== 'flat' && rule.discount_type !== 'percentage') {
|
|
return false;
|
|
}
|
|
|
|
// Count how many of this product are in cart (non-discounted items)
|
|
const fullPriceCount = this.cart.filter(item =>
|
|
parseInt(item.id) === parseInt(product.id) && !item.isDiscounted
|
|
).length;
|
|
|
|
// Count how many discounted items are already in cart
|
|
const discountedCount = this.cart.filter(item =>
|
|
parseInt(item.id) === parseInt(product.id) && item.isDiscounted
|
|
).length;
|
|
|
|
// For "buy 2 get 1 discount" rules:
|
|
// - buy_qty = 2 means you need to buy 2 total (1 full price + 1 discounted)
|
|
// - So user needs (buy_qty - get_qty) = 1 full price item to qualify for 1 discounted
|
|
const fullPriceNeeded = rule.buy_qty - rule.get_qty;
|
|
|
|
// Check if we have enough full-price items
|
|
if (fullPriceCount < fullPriceNeeded) {
|
|
return false;
|
|
}
|
|
|
|
// Calculate max allowed discounted items based on full-price items
|
|
let maxDiscounted = rule.get_qty;
|
|
if (rule.recursive) {
|
|
// For each set of (buy_qty - get_qty) full-price items, allow get_qty discounted
|
|
maxDiscounted = Math.floor(fullPriceCount / fullPriceNeeded) * rule.get_qty;
|
|
}
|
|
|
|
// Can add if we haven't reached the max
|
|
return discountedCount < maxDiscounted;
|
|
},
|
|
|
|
/**
|
|
* Get the discounted price for BOGO item
|
|
* @param {Object} product
|
|
* @returns {string|null}
|
|
*/
|
|
getBogoDiscountedPrice(product) {
|
|
if (!product || !product.bogo_rules || product.bogo_rules.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
const rule = product.bogo_rules[0];
|
|
|
|
if (rule.discount_type === 'flat') {
|
|
return (parseFloat(product.price) - rule.discount_value).toFixed(2);
|
|
} else if (rule.discount_type === 'percentage') {
|
|
const discount = parseFloat(product.price) * (rule.discount_value / 100);
|
|
return (parseFloat(product.price) - discount).toFixed(2);
|
|
}
|
|
|
|
return null;
|
|
},
|
|
|
|
/**
|
|
* Get BOGO label for a product
|
|
* @param {Object} product
|
|
* @returns {string|null}
|
|
*/
|
|
getBogoLabel(product) {
|
|
return BogoService.getBogoLabel(product);
|
|
},
|
|
|
|
/**
|
|
* Check if product has BOGO rules
|
|
* @param {Object} product
|
|
* @returns {boolean}
|
|
*/
|
|
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;
|
|
},
|
|
|
|
/**
|
|
* Check if Y product discount can be added (respects get_qty limit)
|
|
* @param {Object} yProduct - The upsell product
|
|
* @param {Object} discount - The discount info from getYProductBogoDiscount
|
|
* @returns {boolean}
|
|
*/
|
|
canAddYProductDiscount(yProduct, discount) {
|
|
if (!this.activeProduct || !discount) return false;
|
|
|
|
// Find the matching rule from active product
|
|
const rule = this.activeProduct.bogo_rules?.find(r => r.rule_id === discount.rule_id);
|
|
if (!rule) return false;
|
|
|
|
// Count how many of this Y product are already in cart with discount
|
|
const discountedCount = this.cart.filter(item =>
|
|
parseInt(item.id) === parseInt(yProduct.id) &&
|
|
item.isDiscounted &&
|
|
item.ruleId === discount.rule_id
|
|
).length;
|
|
|
|
// Count how many of the X product (active product) are in cart
|
|
const xProductCount = this.cart.filter(item =>
|
|
parseInt(item.id) === parseInt(this.activeProduct.id) && !item.isFree
|
|
).length;
|
|
|
|
// Calculate how many times the rule applies
|
|
let timesApplied = 1;
|
|
if (rule.recursive && xProductCount >= rule.buy_qty) {
|
|
timesApplied = Math.floor(xProductCount / rule.buy_qty);
|
|
}
|
|
|
|
// Maximum Y products allowed = timesApplied * get_qty
|
|
const maxAllowed = timesApplied * rule.get_qty;
|
|
|
|
// Can add if we haven't reached the max
|
|
return discountedCount < maxAllowed;
|
|
},
|
|
|
|
/**
|
|
* Clear the cart
|
|
*/
|
|
clearCart() {
|
|
this.cart = [];
|
|
this.bogoDiscount = 0;
|
|
this.bogoFreeItems = [];
|
|
this.appliedBogoRules = [];
|
|
},
|
|
|
|
/**
|
|
* Reset cart and shipping for new order
|
|
*/
|
|
resetCart() {
|
|
this.cart = [];
|
|
this.shipping = '8.95';
|
|
this.bogoDiscount = 0;
|
|
this.bogoFreeItems = [];
|
|
this.appliedBogoRules = [];
|
|
}
|
|
};
|
|
},
|
|
|
|
/**
|
|
* Get computed properties for Alpine.js component
|
|
* @returns {Object}
|
|
*/
|
|
getComputed() {
|
|
return {
|
|
/**
|
|
* 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
|
|
.reduce((sum, item) => sum + parseFloat(item.price), 0)
|
|
.toFixed(2);
|
|
},
|
|
|
|
/**
|
|
* Calculate total including shipping
|
|
* @returns {string}
|
|
*/
|
|
total() {
|
|
const itemsTotal = this.cart
|
|
.reduce((sum, item) => sum + parseFloat(item.price), 0);
|
|
const shipping = parseFloat(this.shipping) || 0;
|
|
return (itemsTotal + shipping).toFixed(2);
|
|
},
|
|
|
|
/**
|
|
* Get count of free items in cart
|
|
* @returns {number}
|
|
*/
|
|
freeItemsCount() {
|
|
return this.cart.filter(item => item.isFree).length;
|
|
}
|
|
};
|
|
}
|
|
};
|