2026-01-16 17:23:23 +01:00

574 lines
23 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
* For products with Y discount and get_qty > 1, this ONLY adds items (use - button to remove)
* For regular products, this toggles (add/remove)
* @param {Object} product
*/
toggleUpsell(product) {
// Check if this product has a Y discount from active product
const yDiscount = this.getYProductBogoDiscount(product);
// For Y-products with discount, ONLY ADD (never remove via this button)
if (yDiscount) {
console.log('[Y-PRODUCT DEBUG] Adding Y-product with discount:', {
product_name: product.name,
has_discount: true
});
// Check if we can add more discounted items (respect get_qty limit)
if (!this.canAddYProductDiscount(product, yDiscount)) {
console.log('[Y-PRODUCT DEBUG] Cannot add more discounted items - limit reached');
alert('Maximum aantal kortingsproducten bereikt voor deze actie.');
return;
}
// Add with discounted price
const discountedPrice = this.getYProductDiscountedPrice(product);
const discountLabel = this.getYProductDiscountLabel(product);
console.log('[Y-PRODUCT DEBUG] Adding with discount:', {
discountedPrice,
discountLabel,
originalPrice: product.price
});
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 {
// Regular product without discount - toggle behavior
const idx = this.cart.findIndex(i => parseInt(i.id) === parseInt(product.id) && !i.isFree);
if (idx > -1) {
console.log('[Y-PRODUCT DEBUG] Removing regular product from cart:', product.name);
this.cart.splice(idx, 1);
} else {
console.log('[Y-PRODUCT DEBUG] Adding regular product with regular price');
this.cart.push({
id: parseInt(product.id),
name: product.name,
price: product.price,
isFree: false
});
}
}
this.recalculateBogo();
},
/**
* Remove one Y-product from cart (for - button)
* @param {Object} product
*/
removeOneYProduct(product) {
console.log('[Y-PRODUCT DEBUG] Removing one Y-product:', product.name);
// Find last occurrence of this product and remove it
for (let i = this.cart.length - 1; i >= 0; i--) {
if (parseInt(this.cart[i].id) === parseInt(product.id) && !this.cart[i].isFree) {
console.log('[Y-PRODUCT DEBUG] Found and removing item at index:', i);
this.cart.splice(i, 1);
this.recalculateBogo();
return;
}
}
},
/**
* Recalculate BOGO discounts based on current cart
*/
recalculateBogo() {
console.log('[CART DEBUG] recalculateBogo() called, current cart:', this.cart);
// Remove existing auto-added free items from cart (but keep manually added discounted items)
this.cart = this.cart.filter(item => !item.isFree);
console.log('[CART DEBUG] Cart after removing free items:', this.cart);
// Calculate new BOGO
const bogoResult = BogoService.calculateBogoForCart(this.cart, this.products);
console.log('[CART DEBUG] BOGO calculation result:', {
discountAmount: bogoResult.discountAmount,
freeItemsCount: bogoResult.freeItems.length,
freeItems: bogoResult.freeItems,
appliedRulesCount: bogoResult.appliedRules.length
});
this.bogoDiscount = bogoResult.discountAmount;
this.bogoFreeItems = bogoResult.freeItems;
this.appliedBogoRules = bogoResult.appliedRules;
// Add free items to cart (for both buy_x_get_x and buy_x_get_y with free_product)
for (const freeItem of bogoResult.freeItems) {
console.log('[CART DEBUG] Adding free item to cart:', freeItem);
this.cart.push({
id: parseInt(freeItem.id),
variation_id: 0,
name: freeItem.name,
price: freeItem.price,
originalPrice: freeItem.originalPrice,
isFree: true,
ruleId: freeItem.ruleId
});
}
console.log('[CART DEBUG] Final cart after BOGO:', this.cart);
},
/**
* 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];
console.log('[BOGO ADD DEBUG] Adding discounted item:', {
product_name: product.name,
rule_id: rule.rule_id,
discount_type: rule.discount_type,
discount_value: rule.discount_value,
get_qty: rule.get_qty,
buy_qty: rule.buy_qty,
recursive: rule.recursive
});
// Check if this is a discount type rule (not free)
if (rule.discount_type !== 'flat' && rule.discount_type !== 'percentage') {
console.log('[BOGO ADD DEBUG] Not a discount type rule, skipping');
return;
}
// Count current items
const fullPriceCount = this.cart.filter(item =>
parseInt(item.id) === parseInt(product.id) && !item.isDiscounted
).length;
const discountedCount = this.cart.filter(item =>
parseInt(item.id) === parseInt(product.id) && item.isDiscounted
).length;
console.log('[BOGO ADD DEBUG] Current cart state:', {
fullPriceCount,
discountedCount,
maxAllowed: rule.get_qty
});
// 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';
}
console.log('[BOGO ADD DEBUG] Calculated price:', {
originalPrice: product.price,
discountedPrice,
discountLabel
});
// 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
});
console.log('[BOGO ADD DEBUG] Item added to cart. New discounted count:', discountedCount + 1);
},
/**
* 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) {
console.log('[BOGO CAN ADD DEBUG] Not enough full-price items:', {
product_name: product.name,
fullPriceCount,
fullPriceNeeded,
get_qty: rule.get_qty
});
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;
}
console.log('[BOGO CAN ADD DEBUG] Checking if can add:', {
product_name: product.name,
fullPriceCount,
discountedCount,
maxDiscounted,
get_qty: rule.get_qty,
canAdd: discountedCount < maxDiscounted,
remaining: maxDiscounted - discountedCount
});
// 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;
console.log('[Y-PRODUCT CAN ADD DEBUG] Checking Y product limit:', {
yProduct_name: yProduct.name,
xProduct_name: this.activeProduct.name,
xProductCount,
discountedCount,
timesApplied,
get_qty: rule.get_qty,
maxAllowed,
canAdd: discountedCount < maxAllowed,
remaining: maxAllowed - discountedCount
});
// Can add if we haven't reached the max
return discountedCount < maxAllowed;
},
/**
* Get count of Y product in cart (for display)
* @param {Object} yProduct - The upsell product
* @returns {number}
*/
getYProductCountInCart(yProduct) {
return this.cart.filter(item =>
parseInt(item.id) === parseInt(yProduct.id) && !item.isFree
).length;
},
/**
* Get remaining quantity that can be added for Y product
* @param {Object} yProduct - The upsell product
* @returns {number}
*/
getYProductRemainingQty(yProduct) {
const discount = this.getYProductBogoDiscount(yProduct);
if (!discount || !this.activeProduct) return 0;
const rule = this.activeProduct.bogo_rules?.find(r => r.rule_id === discount.rule_id);
if (!rule) return 0;
const discountedCount = this.cart.filter(item =>
parseInt(item.id) === parseInt(yProduct.id) &&
item.isDiscounted &&
item.ruleId === discount.rule_id
).length;
const xProductCount = this.cart.filter(item =>
parseInt(item.id) === parseInt(this.activeProduct.id) && !item.isFree
).length;
let timesApplied = 1;
if (rule.recursive && xProductCount >= rule.buy_qty) {
timesApplied = Math.floor(xProductCount / rule.buy_qty);
}
const maxAllowed = timesApplied * rule.get_qty;
return Math.max(0, maxAllowed - discountedCount);
},
/**
* 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;
}
};
}
};