307 lines
11 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 {
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 free items from cart
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
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);
},
/**
* 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, no BOGO discount)
* @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
* @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);
},
/**
* Get count of free items in cart
* @returns {number}
*/
freeItemsCount() {
return this.cart.filter(item => item.isFree).length;
}
};
}
};