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