2026-01-14 12:57:53 +01:00

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;
}
};
}
};