Quantity bug fix

This commit is contained in:
Mark Pinkster 2026-01-16 17:23:23 +01:00
parent ae6c5b0ca4
commit cdc3b88b65
5 changed files with 431 additions and 149 deletions

View File

@ -21,7 +21,7 @@ function handleCreateOrder(): void
$input['payment_method'] = 'cod';
$input['payment_method_title'] = 'Sales Panel Order';
$input['status'] = 'on-hold';
$input['status'] = 'processing-unpaid';
// IMPORTANT: Disable automatic price calculation
// This ensures WooCommerce uses the prices from the sales panel

View File

@ -23,7 +23,7 @@
<!-- Load JavaScript modules with cache busting -->
<script>
// Change this version number to bust cache for all JS files
const APP_VERSION = '1.2.0';
const APP_VERSION = '1.5.1';
const scripts = [
'js/services/api.js',
@ -171,45 +171,52 @@
class="font-bold mb-4 text-slate-400 uppercase text-[10px] tracking-widest border-b pb-2 text-center italic">
Producten</h2>
<!-- Custom searchable dropdown for main product -->
<div class="relative mb-6" x-data="{ open: false }" @click.away="open = false">
<button type="button" @click="open = !open"
class="w-full border-2 border-slate-100 p-5 rounded-2xl font-black text-slate-700 bg-slate-50 outline-none focus:border-blue-500 shadow-sm text-left flex justify-between items-center">
<span x-text="selectedProductId ? products.find(p => p.id == selectedProductId)?.name : '-- Kies Hoofdproduct --'" class="truncate"></span>
<svg class="w-5 h-5 text-slate-400 transition-transform" :class="{ 'rotate-180': open }" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
</svg>
</button>
<div x-show="open" x-cloak
class="absolute z-50 w-full mt-2 bg-white border-2 border-slate-200 rounded-2xl shadow-xl overflow-hidden">
<div class="p-3 border-b border-slate-100">
<input type="text" x-model="productSearch" placeholder="Zoek product..."
class="w-full border border-slate-200 p-3 rounded-xl text-sm outline-none focus:border-blue-500"
@click.stop>
</div>
<div class="max-h-60 overflow-y-auto custom-scrollbar">
<div @click="selectedProductId = ''; selectProduct(); open = false; productSearch = ''"
class="p-4 hover:bg-slate-50 cursor-pointer text-slate-500 font-medium text-sm">
-- Kies Hoofdproduct --
<!-- Custom searchable dropdown for product selection with Add button -->
<div class="flex gap-2 mb-6">
<div class="relative flex-1" x-data="{ open: false }" @click.away="open = false">
<button type="button" @click="open = !open"
class="w-full border-2 border-slate-100 p-5 rounded-2xl font-black text-slate-700 bg-slate-50 outline-none focus:border-blue-500 shadow-sm text-left flex justify-between items-center">
<span x-text="selectedProductId ? products.find(p => p.id == selectedProductId)?.name : '-- Kies Product --'" class="truncate"></span>
<svg class="w-5 h-5 text-slate-400 transition-transform" :class="{ 'rotate-180': open }" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
</svg>
</button>
<div x-show="open" x-cloak
class="absolute z-50 w-full mt-2 bg-white border-2 border-slate-200 rounded-2xl shadow-xl overflow-hidden">
<div class="p-3 border-b border-slate-100">
<input type="text" x-model="productSearch" placeholder="Zoek product..."
class="w-full border border-slate-200 p-3 rounded-xl text-sm outline-none focus:border-blue-500"
@click.stop>
</div>
<template x-for="p in filteredProducts" :key="p.id">
<div @click="selectedProductId = p.id; selectProduct(); open = false; productSearch = ''"
class="p-4 hover:bg-blue-50 cursor-pointer font-bold text-sm text-slate-700"
:class="{ 'bg-blue-100': selectedProductId == p.id }">
<div class="flex items-center justify-between">
<span x-text="p.name"></span>
<span x-show="p.bogo_rules && p.bogo_rules.length > 0"
x-text="p.bogo_rules[0]?.badge_text || p.bogo_rules[0]?.label"
class="ml-2 px-2 py-1 bg-red-500 text-white text-[9px] font-black rounded-full max-w-48 truncate uppercase">
</span>
</div>
<div class="max-h-60 overflow-y-auto custom-scrollbar">
<div @click="selectedProductId = ''; activeProduct = null; variations = []; open = false; productSearch = ''"
class="p-4 hover:bg-slate-50 cursor-pointer text-slate-500 font-medium text-sm">
-- Kies Product --
</div>
<template x-for="p in filteredProducts" :key="p.id">
<div @click="selectedProductId = p.id; selectProduct(); open = false; productSearch = ''"
class="p-4 hover:bg-blue-50 cursor-pointer font-bold text-sm text-slate-700"
:class="{ 'bg-blue-100': selectedProductId == p.id }">
<div class="flex items-center justify-between">
<span x-text="p.name"></span>
<span x-show="p.bogo_rules && p.bogo_rules.length > 0"
x-text="p.bogo_rules[0]?.badge_text || p.bogo_rules[0]?.label"
class="ml-2 px-2 py-1 bg-red-500 text-white text-[9px] font-black rounded-full max-w-48 truncate uppercase">
</span>
</div>
</div>
</template>
<div x-show="filteredProducts.length === 0" class="p-4 text-slate-400 text-sm text-center">
Geen producten gevonden
</div>
</template>
<div x-show="filteredProducts.length === 0" class="p-4 text-slate-400 text-sm text-center">
Geen producten gevonden
</div>
</div>
</div>
<button @click="addProductToCart()"
:disabled="!activeProduct"
class="bg-blue-600 text-white px-8 py-5 rounded-2xl font-black text-sm uppercase shadow-md hover:bg-blue-700 transition active:scale-95 disabled:opacity-30 disabled:cursor-not-allowed whitespace-nowrap">
Add
</button>
</div>
<!-- BOGO Actie Banner - Only for buy_x_get_x rules -->
@ -258,22 +265,37 @@
<div x-show="getYProductBogoDiscount(u)">
<div class="p-4 bg-gradient-to-r from-red-500 to-pink-500 rounded-2xl text-white shadow-lg">
<div class="flex items-center justify-between gap-3">
<div class="flex items-center gap-3">
<div class="flex items-center gap-3 flex-1">
<div class="bg-white/20 p-2 rounded-full">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v13m0-13V6a2 2 0 112 2h-2zm0 0V5.5A2.5 2.5 0 109.5 8H12zm-7 4h14M5 12a2 2 0 110-4h14a2 2 0 110 4M5 12v7a2 2 0 002 2h10a2 2 0 002-2v-7" />
</svg>
</div>
<div>
<div class="flex-1">
<p class="font-black text-sm uppercase tracking-wide" x-text="u.name"></p>
<p class="text-[10px] opacity-80" x-text="getYProductDiscountLabel(u)"></p>
<p x-show="getYProductCountInCart(u) > 0" class="text-[10px] opacity-90 font-bold mt-1">
<span x-text="getYProductCountInCart(u)"></span> in winkelwagen
<!-- <span x-show="getYProductRemainingQty(u) > 0">
<span x-text="getYProductRemainingQty(u)"></span> nog beschikbaar
</span> -->
</p>
</div>
</div>
<button @click="toggleUpsell(u)"
:class="isInCart(u.id) ? 'bg-slate-700' : 'bg-white'"
class="text-red-500 px-4 py-2 rounded-xl font-black text-xs uppercase shadow-md hover:bg-red-50 transition disabled:opacity-50 disabled:cursor-not-allowed whitespace-nowrap">
<span x-text="isInCart(u.id) ? 'Toegevoegd' : ('+ €' + getYProductDiscountedPrice(u))"></span>
</button>
<div class="flex gap-2">
<button
x-show="getYProductCountInCart(u) > 0"
@click.stop="removeOneYProduct(u)"
class="bg-slate-700 text-white px-3 py-2 rounded-xl font-black text-xs uppercase shadow-md hover:bg-slate-600 transition">
-
</button>
<button
@click.stop="toggleUpsell(u)"
:disabled="!canAddYProductDiscount(u, getYProductBogoDiscount(u))"
class="bg-white text-red-500 px-4 py-2 rounded-xl font-black text-xs uppercase shadow-md hover:bg-red-50 transition disabled:opacity-50 disabled:cursor-not-allowed whitespace-nowrap">
<span x-text="'+ €' + getYProductDiscountedPrice(u)"></span>
</button>
</div>
</div>
</div>
<!-- Upsell information text for BOGO products -->
@ -300,50 +322,6 @@
</div>
</template>
</div>
<div class="mt-8 border-t pt-6">
<h2 class="font-bold mb-4 text-slate-400 uppercase text-[10px] tracking-widest italic text-center">
Voeg extra product toe</h2>
<div class="flex gap-2">
<!-- Custom searchable dropdown for extra products -->
<div class="relative flex-1" x-data="{ openExtra: false }" @click.away="openExtra = false">
<button type="button" @click="openExtra = !openExtra"
class="w-full border-2 border-slate-100 p-3 rounded-xl font-bold text-xs bg-slate-50 outline-none focus:border-green-500 text-left flex justify-between items-center text-slate-700">
<span x-text="extraProductId ? (products.find(p => p.id == extraProductId)?.name + ' (€' + products.find(p => p.id == extraProductId)?.price + ')') : '-- Voeg product toe --'" class="truncate"></span>
<svg class="w-4 h-4 text-slate-400 transition-transform flex-shrink-0 ml-2" :class="{ 'rotate-180': openExtra }" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
</svg>
</button>
<div x-show="openExtra" x-cloak
class="absolute z-50 w-full mt-2 bg-white border-2 border-slate-200 rounded-2xl shadow-xl overflow-hidden">
<div class="p-3 border-b border-slate-100">
<input type="text" x-model="extraProductSearch" placeholder="Zoek product..."
class="w-full border border-slate-200 p-2 rounded-xl text-sm outline-none focus:border-green-500"
@click.stop>
</div>
<div class="max-h-60 overflow-y-auto custom-scrollbar">
<div @click="extraProductId = ''; openExtra = false; extraProductSearch = ''"
class="p-3 hover:bg-slate-50 cursor-pointer text-slate-500 font-medium text-xs">
-- Voeg product toe --
</div>
<template x-for="p in filteredExtraProducts" :key="'extra-'+p.id">
<div @click="extraProductId = p.id; openExtra = false; extraProductSearch = ''"
class="p-3 hover:bg-green-50 cursor-pointer font-bold text-xs text-slate-700"
:class="{ 'bg-green-100': extraProductId == p.id }">
<span x-text="p.name + ' (€' + p.price + ')'"></span>
</div>
</template>
<div x-show="filteredExtraProducts.length === 0" class="p-3 text-slate-400 text-xs text-center">
Geen producten gevonden
</div>
</div>
</div>
</div>
<button @click="addExtraItem()"
class="bg-green-600 text-white px-6 py-3 rounded-xl font-black text-[10px] uppercase shadow-md hover:bg-green-700 transition active:scale-95">Add</button>
</div>
</div>
</div>
<div class="col-span-12 lg:col-span-3">

View File

@ -62,38 +62,55 @@ const CartComponent = {
/**
* 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) {
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);
// 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
});
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
});
// 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 {
// Add with regular price
console.log('[Y-PRODUCT DEBUG] Adding regular product with regular price');
this.cart.push({
id: parseInt(product.id),
name: product.name,
@ -105,23 +122,52 @@ const CartComponent = {
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 (only for buy_x_get_x rules)
// buy_x_get_y items are NOT auto-added, user must add them manually
// 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,
@ -132,6 +178,8 @@ const CartComponent = {
ruleId: freeItem.ruleId
});
}
console.log('[CART DEBUG] Final cart after BOGO:', this.cart);
},
/**
@ -146,11 +194,37 @@ const CartComponent = {
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;
@ -164,6 +238,12 @@ const CartComponent = {
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({
@ -176,6 +256,8 @@ const CartComponent = {
isDiscounted: true,
ruleId: rule.rule_id
});
console.log('[BOGO ADD DEBUG] Item added to cart. New discounted count:', discountedCount + 1);
},
/**
@ -213,6 +295,12 @@ const CartComponent = {
// 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;
}
@ -223,6 +311,16 @@ const CartComponent = {
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;
},
@ -354,10 +452,64 @@ const CartComponent = {
// 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
*/

View File

@ -14,6 +14,9 @@ const ProductsComponent = {
selectedVariationId: '',
variations: [],
extraProductId: '',
extraProduct: null,
extraVariations: [],
extraSelectedVariationId: '',
recommendedOptions: [],
productSearch: '',
extraProductSearch: ''
@ -50,21 +53,92 @@ const ProductsComponent = {
},
/**
* Handle main product selection
* Handle main product selection (no longer auto-adds to cart)
*/
selectProduct() {
const p = this.products.find(x => x.id == this.selectedProductId);
if (!p) return;
console.log('[PRODUCT DEBUG] Selected product:', p);
this.activeProduct = p;
this.variations = p.variation_details || [];
this.cart = [];
this.bogoDiscount = 0;
this.bogoFreeItems = [];
this.appliedBogoRules = [];
this.selectedVariationId = '';
// Don't auto-add to cart anymore - wait for user to click Add button
// Don't clear cart anymore - allow adding multiple products
},
/**
* Add selected product to cart (new method for Add button)
*/
addProductToCart() {
if (!this.activeProduct) return;
const p = this.activeProduct;
// If variable product, require variation selection
if (p.type === 'variable') {
if (!this.selectedVariationId) {
alert('Selecteer eerst een variatie');
return;
}
const v = this.variations.find(x => x.id == this.selectedVariationId);
if (!v) return;
console.log('[PRODUCT DEBUG] Adding variable product with variation:', v);
this.cart.push({
id: parseInt(p.id),
variation_id: parseInt(v.id),
name: p.name + ' - ' + this.getVarName(v),
price: v.price,
isFree: false
});
} else {
// Simple product
console.log('[PRODUCT DEBUG] Adding simple product');
this.cart.push({
id: parseInt(p.id),
name: p.name,
price: p.price,
isFree: false
});
}
this.recalculateBogo();
this.loadRecommendations(p);
// Keep selection visible (don't reset)
},
/**
* Handle variation selection (no longer auto-adds to cart)
*/
selectVariation() {
// Just store the selection, don't add to cart
// User must click Add button to add to cart
console.log('[PRODUCT DEBUG] Variation selected:', this.selectedVariationId);
},
/**
* Select extra product (handles variations like main product)
*/
selectExtraProduct() {
const p = this.products.find(x => x.id == this.extraProductId);
if (!p) return;
console.log('[EXTRA PRODUCT DEBUG] Selected product:', p);
this.extraProduct = p;
this.extraVariations = p.variation_details || [];
this.extraSelectedVariationId = '';
// If not a variable product, add directly
if (p.type !== 'variable') {
console.log('[EXTRA PRODUCT DEBUG] Simple product, adding to cart');
this.cart.push({
id: parseInt(p.id),
name: p.name,
@ -72,49 +146,56 @@ const ProductsComponent = {
isFree: false
});
this.recalculateBogo();
this.loadRecommendations(p);
this.mergeRecommendations(p);
this.extraProductId = '';
this.extraProduct = null;
}
// If variable product, wait for variation selection
else {
console.log('[EXTRA PRODUCT DEBUG] Variable product, waiting for variation selection. Variations:', this.extraVariations);
}
},
/**
* Handle variation selection
* Handle extra product variation selection
*/
selectVariation() {
const v = this.variations.find(x => x.id == this.selectedVariationId);
if (!v) return;
selectExtraVariation() {
const v = this.extraVariations.find(x => x.id == this.extraSelectedVariationId);
if (!v || !this.extraProduct) return;
this.cart = [{
id: parseInt(this.activeProduct.id),
console.log('[EXTRA PRODUCT DEBUG] Selected variation:', v);
this.cart.push({
id: parseInt(this.extraProduct.id),
variation_id: parseInt(v.id),
name: this.activeProduct.name + ' - ' + this.getVarName(v),
name: this.extraProduct.name + ' - ' + this.getVarName(v),
price: v.price,
isFree: false
}];
});
this.recalculateBogo();
this.loadRecommendations(this.activeProduct);
this.mergeRecommendations(this.extraProduct);
// Reset extra product state
this.extraProductId = '';
this.extraProduct = null;
this.extraVariations = [];
this.extraSelectedVariationId = '';
},
/**
* Add extra product to cart
* Cancel extra product selection
*/
addExtraItem() {
const p = this.products.find(x => x.id == this.extraProductId);
if (!p) return;
this.cart.push({
id: parseInt(p.id),
name: p.name,
price: p.price,
isFree: false
});
this.recalculateBogo();
cancelExtraProduct() {
this.extraProductId = '';
this.extraProduct = null;
this.extraVariations = [];
this.extraSelectedVariationId = '';
},
/**
* Load product recommendations
* @param {Object} product
* @param {Object} product
*/
loadRecommendations(product) {
this.recommendedOptions = [];
@ -136,6 +217,42 @@ const ProductsComponent = {
this.recommendedOptions = this.products.filter(p => ids.includes(parseInt(p.id)));
},
/**
* Merge recommendations from an additional product (for extra products)
* @param {Object} product
*/
mergeRecommendations(product) {
console.log('[MERGE RECOMMENDATIONS] Merging recommendations for:', product.name);
// Get existing recommendation IDs
const existingIds = this.recommendedOptions.map(p => parseInt(p.id));
console.log('[MERGE RECOMMENDATIONS] Existing IDs:', existingIds);
// Get new product's recommendations
let newIds = [];
if (product.recommended_ids && product.recommended_ids.length) {
newIds = product.recommended_ids.map(id => parseInt(id));
} else {
// Fallback: combine Woo upsells + cross-sells
const ups = (product.upsell_ids || []).map(id => parseInt(id));
const crs = (product.cross_sell_ids || []).map(id => parseInt(id));
newIds = [...ups, ...crs];
}
newIds = [...new Set(newIds)].filter(Boolean);
console.log('[MERGE RECOMMENDATIONS] New IDs to add:', newIds);
// Merge: add new recommendations that aren't already in the list
const idsToAdd = newIds.filter(id => !existingIds.includes(id));
console.log('[MERGE RECOMMENDATIONS] IDs to add (filtered):', idsToAdd);
const newRecommendations = this.products.filter(p => idsToAdd.includes(parseInt(p.id)));
console.log('[MERGE RECOMMENDATIONS] New recommendations to add:', newRecommendations.map(p => p.name));
this.recommendedOptions = [...this.recommendedOptions, ...newRecommendations];
console.log('[MERGE RECOMMENDATIONS] Final recommendations count:', this.recommendedOptions.length);
},
/**
* Get variation name from attributes
* @param {Object} v - Variation object
@ -154,6 +271,10 @@ const ProductsComponent = {
this.activeProduct = null;
this.variations = [];
this.recommendedOptions = [];
this.extraProductId = '';
this.extraProduct = null;
this.extraVariations = [];
this.extraSelectedVariationId = '';
}
};
},

View File

@ -142,27 +142,58 @@ const BogoService = {
// The user clicks a button to add the discounted item
} else if (rule.type === 'buy_x_get_y' && rule.get_product_ids) {
// Different product (Y) is free or discounted
// For buy_x_get_y, we DON'T automatically add items to cart
// The user must manually add them via the upsell section
// We only track the discount amount for display purposes
console.log('[BOGO DEBUG] Processing buy_x_get_y rule:', {
rule_id: rule.rule_id,
rule_title: rule.rule_title,
get_product_ids: rule.get_product_ids,
discount_type: rule.discount_type,
discount_value: rule.discount_value,
freeQty: freeQty
});
// For buy_x_get_y with free_product, we AUTO-ADD the free items
// For other discount types, user must manually add them
for (const freeProductId of rule.get_product_ids) {
const freeProduct = products.find(p => p.id == freeProductId);
if (!freeProduct) continue;
if (!freeProduct) {
console.log('[BOGO DEBUG] Free product not found:', freeProductId);
continue;
}
const freeProductPrice = parseFloat(freeProduct.price);
console.log('[BOGO DEBUG] Found free product:', {
id: freeProduct.id,
name: freeProduct.name,
price: freeProductPrice
});
if (rule.discount_type === 'free_product' || rule.discount_value >= 100) {
// 100% korting - product Y is gratis (but not auto-added)
// 100% korting - product Y is gratis - AUTO-ADD to cart
result.discountAmount += freeQty * freeProductPrice;
console.log('[BOGO DEBUG] Adding free items to cart:', freeQty);
// Add get_qty free items
for (let i = 0; i < freeQty; i++) {
result.freeItems.push({
id: freeProduct.id,
name: freeProduct.name + ' (GRATIS)',
price: '0.00',
originalPrice: freeProduct.price,
isFree: true,
ruleId: rule.rule_id
});
}
} else if (rule.discount_type === 'percentage') {
// Percentage korting op product Y (but not auto-added)
const discountPerItem = freeProductPrice * (rule.discount_value / 100);
result.discountAmount += freeQty * discountPerItem;
console.log('[BOGO DEBUG] Percentage discount (not auto-added):', rule.discount_value + '%');
} else if (rule.discount_type === 'flat') {
// Vaste korting op product Y (but not auto-added)
const discountedPrice = Math.max(0, freeProductPrice - rule.discount_value).toFixed(2);
const actualDiscount = freeProductPrice - parseFloat(discountedPrice);
result.discountAmount += freeQty * actualDiscount;
console.log('[BOGO DEBUG] Flat discount (not auto-added):', rule.discount_value);
}
}
}