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