Add customer discount display to products

Introduced a reusable CustomerProductDiscountDisplay component to show applicable discounts per product. Updated related files to pass necessary props and refactor discount handling for accuracy and consistency.
This commit is contained in:
Jepp9350
2025-03-07 11:16:16 +01:00
parent 639ed6b7ab
commit 62c7b50120
5 changed files with 368 additions and 101 deletions
+102 -88
View File
@@ -3,6 +3,8 @@ import { ref, defineProps, defineEmits } from 'vue';
import { Colors } from "@/ThemeConfig.vue";
import { showPopper, popperBoxProductAddonHtml, removePopperIfOpen } from "@/components/displays/PopperDefault.vue";
import Swal from "sweetalert2";
import CustomerProductDiscountDisplay
from "@/components/displays/department/pos/displays/CustomerProductDiscountDisplay.vue";
const expandIcon = ref(null);
@@ -10,6 +12,7 @@ const emit = defineEmits(['add-to-cart']);
const props = defineProps({
id: Number,
product: Object,
name: String,
description: String,
price: Number,
@@ -22,7 +25,8 @@ const props = defineProps({
addProductAddon: Function,
subtractProductAddon: Function,
unselect: Function,
setProductAddonNote: Function
setProductAddonNote: Function,
customerDiscounts: Array,
});
const isAddonSelected = (productId, addonId) => {
@@ -125,109 +129,119 @@ const doesAddonHaveNoteRequirement = (addon) => {
return addon.product.requires_note;
};
</script>
<template>
<div class="pt-2 mb-2 pr-6">
<div class="columns is-multiline pos-product-box" :class="{ 'is-selected': isSelected }">
<!-- Product image -->
<div class="column is-4-desktop">
<img :src="image" alt="Product image" class="image pos-product-image pl-5 mt-4" />
</div>
<!-- Product details -->
<div class="column is-8-desktop">
<p class="title is-6">{{ name }}</p>
<p class="subtitle is-6 mb-0" :style="{ 'color': Colors.global.primaryColor }">
No. {{ id }}</p>
<p class="subtitle is-6">{{ description }}</p>
<!-- White space, to accommodate the design requirements -->
<p>&nbsp;</p>
<p class="subtitle is-6 has-text-black">{{ price }} Kr.</p>
</div>
<!-- Expandable product details -->
<div v-if="isSelected" class="column is-12-desktop">
<!-- Addons -->
<div class="buttons has-addons is-centered mt-3 mb-0 pl-6" v-if="addons.length > 0">
<div
class="buttons has-addons is-small is-fullwidth is-flex-wrap-nowrap"
v-for="addon in addons"
:key="addon.id"
:class="{ 'is-link': isAddonSelected(props.id, addon.option_id), 'is-light': !isAddonSelected(props.id, addon.option_id) }"
>
<!-- Addon ( + ) -->
<button
class="button is-small"
@click="onBeforeAddProductAddon(addon, () => addProductAddon(
props.id,
addon.option_id
))"
>
<span class="icon is-small">
<i class="fas fa-plus"></i>
</span>
</button>
<!-- quantity (if any), Addon name, price -->
<button
class="button is-small"
<div>
<!-- Discount badge (if any) -->
<div class="is-pulled-right">
<CustomerProductDiscountDisplay
v-bind:product="props.product"
v-bind:customer_discounts="props.customerDiscounts"
/>
</div>
<!-- Product box -->
<div class="pt-2 mb-2 pr-6">
<div class="columns is-multiline pos-product-box" :class="{ 'is-selected': isSelected }">
<!-- Product image -->
<div class="column is-4-desktop">
<img :src="image" alt="Product image" class="image pos-product-image pl-5 mt-4" />
</div>
<!-- Product details -->
<div class="column is-8-desktop">
<!-- Actual product details -->
<p class="title is-6">{{ name }}</p>
<p class="subtitle is-6 mb-0" :style="{ 'color': Colors.global.primaryColor }">
No. {{ id }}</p>
<p class="subtitle is-6">{{ description }}</p>
<!-- White space, to accommodate the design requirements -->
<p>&nbsp;</p>
<p class="subtitle is-6 has-text-black">{{ price }} Kr.</p>
</div>
<!-- Expandable product details -->
<div v-if="isSelected" class="column is-12-desktop">
<!-- Addons -->
<div class="buttons has-addons is-centered mt-3 mb-0 pl-6" v-if="addons.length > 0">
<div
class="buttons has-addons is-small is-fullwidth is-flex-wrap-nowrap"
v-for="addon in addons"
:key="addon.id"
:class="{ 'is-link': isAddonSelected(props.id, addon.option_id), 'is-light': !isAddonSelected(props.id, addon.option_id) }"
@click="onBeforeToggleProductAddon(addon, isAddonSelected(props.id, addon.option_id), () => toggleProductAddon(props.id, addon.option_id))"
@mouseenter="showAddonPopper(addon, $event.target)"
@mouseleave="hideAddonPopper(addon, $event.target)"
>
<template v-if="isAddonSelected(props.id, addon.option_id)">
{{ getProductAddonQuantity(props.id, addon.option_id) }} x
</template>
{{ addon.name }} / {{ getAddonPrice(addon.id) }} Kr.
</button>
<!-- Addon ( - ) -->
<!-- Addon ( + ) -->
<button
class="button is-small"
@click="onBeforeAddProductAddon(addon, () => addProductAddon(
props.id,
addon.option_id
))"
>
<span class="icon is-small">
<i class="fas fa-plus"></i>
</span>
</button>
<!-- quantity (if any), Addon name, price -->
<button
class="button is-small"
:class="{ 'is-link': isAddonSelected(props.id, addon.option_id), 'is-light': !isAddonSelected(props.id, addon.option_id) }"
@click="onBeforeToggleProductAddon(addon, isAddonSelected(props.id, addon.option_id), () => toggleProductAddon(props.id, addon.option_id))"
@mouseenter="showAddonPopper(addon, $event.target)"
@mouseleave="hideAddonPopper(addon, $event.target)"
>
<template v-if="isAddonSelected(props.id, addon.option_id)">
{{ getProductAddonQuantity(props.id, addon.option_id) }} x
</template>
{{ addon.name }} / {{ getAddonPrice(addon.id) }} Kr.
</button>
<!-- Addon ( - ) -->
<button
class="button is-small"
:disabled="!isAddonSelected(props.id, addon.option_id)"
@click="subtractProductAddon(
props.id,
addon.option_id
)"
>
<span class="icon is-small">
<i class="fas fa-minus"></i>
</span>
</button>
</div>
</div>
<!-- White space, to accommodate the design requirements -->
<p>&nbsp;</p>
<!-- Add to cart -->
<div class="buttons is-centered mt-2 mb-3 pl-6">
<button
class="button is-small"
:disabled="!isAddonSelected(props.id, addon.option_id)"
@click="subtractProductAddon(
props.id,
addon.option_id
)"
class="button is-link is-small is-fullwidth"
@click="emitAddToCart(id)"
>
<span class="icon is-small">
<i class="fas fa-minus"></i>
<i class="fas fa-cart-plus"></i>
</span>
<span>Tilføj til kurv</span>
</button>
</div>
</div>
<!-- White space, to accommodate the design requirements -->
<p>&nbsp;</p>
<!-- Add to cart -->
<div class="buttons is-centered mt-2 mb-3 pl-6">
<button
class="button is-link is-small is-fullwidth"
@click="emitAddToCart(id)"
<!-- Expander -->
<div class="column is-12-desktop">
<p
class="has-text-centered pl-6"
@click="toggleIsSelected"
@mouseover="expandIcon.classList.add('has-text-link')"
@mouseleave="expandIcon.classList.remove('has-text-link')"
>
<span class="icon is-small">
<i class="fas fa-cart-plus"></i>
<span class="icon is-small" style="color: #c7c7c7;">
<i
class="fas fa-angle-down fa-lg"
:class="{ 'fa-rotate-180': isSelected }"
aria-hidden="true"
ref="expandIcon"
></i>
</span>
<span>Tilføj til kurv</span>
</button>
</p>
</div>
</div>
<!-- Expander -->
<div class="column is-12-desktop">
<p
class="has-text-centered pl-6"
@click="toggleIsSelected"
@mouseover="expandIcon.classList.add('has-text-link')"
@mouseleave="expandIcon.classList.remove('has-text-link')"
>
<span class="icon is-small" style="color: #c7c7c7;">
<i
class="fas fa-angle-down fa-lg"
:class="{ 'fa-rotate-180': isSelected }"
aria-hidden="true"
ref="expandIcon"
></i>
</span>
</p>
</div>
</div>
</div>
</template>
@@ -0,0 +1,258 @@
<script setup>
import { ref, defineProps, watch } from 'vue';
import { SessionUser } from "@/components/session/token/SessionUser.vue";
const props = defineProps({
// Will use the product prop to get the product (To avoid multiple API calls)
product: {
type: Object,
default: null
},
// The Customer discounts
customer_discounts: {
type: Array,
default: []
},
});
/** Define the variables */
// The best discount percentage for the customer, based on the product, category and global discounts
const highestEligibleDiscount = ref(0);
// If the category discounts are enabled for the product (bool)product.apply_category_discount
const isCategoryDiscountsEnabled = ref((props.product.category_id !== null) && (props.product.apply_category_discount));
// If the global discounts are enabled for the product, this is exactly the same as the `isCategoryDiscountsEnabled` variable, but without the category_id check
const isGlobalDiscountsEnabled = ref(props.product.apply_category_discount);
// The product discount (if any)
const discount_product = ref(null);
// The category discount (if any)
const discount_category = ref(null);
// The global discount (if any)
const discount_global = ref(null);
// Show the discounts dropdown
const showDiscountsDropdown = ref(false);
/** Define the functions */
// Check if the customer has a discount on the product
const hasDiscountOnProduct = () => {
return discount_product.value > 0;
}
// Check if the customer has a discount on the category
const hasDiscountOnCategory = () => {
return discount_category.value > 0;
}
// Check if the customer has a global discount
const hasGlobalDiscount = () => {
return discount_global.value > 0;
}
// Set the product discount
const setProductDiscount = (percentage) => {
// Check if the percentage is a number
if (typeof percentage !== 'number') {
// Warn the user
console.error("The percentage is not a number recieved:", percentage);
// If the percentage is not a number, we'll return
return;
}
discount_product.value = percentage;
}
// Set the category discount
const setCategoryDiscount = (percentage) => {
// Check if the percentage is a number
if (typeof percentage !== 'number') {
// Warn the user
console.error("The percentage is not a number recieved:", percentage);
// If the percentage is not a number, we'll return
return;
}
discount_category.value = percentage;
}
// Set the global discount
const setGlobalDiscount = (percentage) => {
// Check if the percentage is a number
if (typeof percentage !== 'number') {
// Warn the user
console.error("The percentage is not a number recieved:", percentage);
// If the percentage is not a number, we'll return
return;
}
discount_global.value = percentage;
}
// Debugging function
const getDiscountDebug = () => {
let line = '#####################'
console.log(line);
console.log("Product: ", props.product);
console.log("Product discount: " + (hasDiscountOnProduct() ? discount_product.value : "No discount set"));
console.log("Category discount: " + (hasDiscountOnCategory() ? discount_category.value : "No discount set"));
console.log("Global discount: " + (hasGlobalDiscount() ? discount_global.value : "No discount set"));
console.log("Category discounts enabled: " + isCategoryDiscountsEnabled.value);
console.log("Global discounts enabled: " + isGlobalDiscountsEnabled.value);
console.log("Customer discounts: ", props.customer_discounts);
console.log("Best discount: " + highestEligibleDiscount.value);
console.log(line);
}
// Parse the customer discounts
const parseCustomerDiscounts = () => {
const tmp_discount_product = props.customer_discounts.find((discount) => discount.product_or_category_id === props.product.id && !discount.is_category)
const tmp_discount_category = props.customer_discounts.find((discount) => discount.product_or_category_id === props.product.category && discount.is_category);
const tmp_discount_global = props.customer_discounts.find((discount) => discount.id === 999999 && discount.is_category);
//console.log("Product discount: ", tmp_discount_product);
//console.log("Category discount: ", tmp_discount_category);
//console.log("Global discount: ", tmp_discount_global);
// Set the product discount
if (tmp_discount_product) {
setProductDiscount(tmp_discount_product.percentage);
}
// Set the category discount
if (tmp_discount_category) {
setCategoryDiscount(tmp_discount_category.percentage);
}
// Set the global discount
if (tmp_discount_global) {
setGlobalDiscount(tmp_discount_global.percentage);
}
}
// Get the best discount for the customer
const getBestDiscount = () => {
// Set the best discount to 0
let tmp_best_discount = 0;
// Check if the product discount is higher than the current best discount
if (hasDiscountOnProduct() && discount_product.value > tmp_best_discount) {
// Since the product discount is higher than the current best discount, we'll set the best discount to the product discount
tmp_best_discount = discount_product.value;
}
// Check if the category discount is higher than the current best discount
if (hasDiscountOnCategory() && discount_category.value > tmp_best_discount) {
// Check if the category discounts are enabled for the product
if (isCategoryDiscountsEnabled.value) {
// Since the category discount is higher than the current best discount, we'll set the best discount to the category discount
tmp_best_discount = discount_category.value;
}
}
// Check if the global discount is higher than the current best discount
if (hasGlobalDiscount() && discount_global.value > tmp_best_discount) {
// Check if the global discounts are enabled for the product
if (isGlobalDiscountsEnabled.value) {
// Since the global discount is higher than the current best discount, we'll set the best discount to the global discount
tmp_best_discount = discount_global.value;
}
}
// Set the highest eligible discount to the best discount
highestEligibleDiscount.value = tmp_best_discount;
// Return the best discount
return tmp_best_discount;
}
/** Call the functions */
// Parse the customer discounts
parseCustomerDiscounts();
// Get the best discount for the customer
getBestDiscount();
// Debug the discount
//getDiscountDebug();
/** Watch the props */
// Watch the product prop, to update the discounts when the discounts change (or initially loads)
watch(() => props.customer_discounts, () => {
console.log("Customer discounts changed");
// Parse the customer discounts
parseCustomerDiscounts();
// Get the best discount for the customer
getBestDiscount();
// Debug the discount
getDiscountDebug();
});
</script>
<template>
<div>
<!-- The dropdown -->
<div class="dropdown is-right" :class="{ 'is-active': showDiscountsDropdown }">
<div class="dropdown-trigger" @mouseover="showDiscountsDropdown = true" @mouseleave="showDiscountsDropdown = false">
<span
aria-haspopup="true"
aria-controls="dropdown-menu"
v-if="highestEligibleDiscount > 0"
class="tag is-warning is-light is-text text-can-not-select"
> -{{ highestEligibleDiscount }}%</span>
</div>
<div class="dropdown-menu" id="dropdown-menu" role="menu">
<div class="dropdown-content py-0">
<div class="list has-overflow-ellipsis" style="width: 340px">
<!-- Best discount -->
<a class="list-item">
<div class="list-item-content">
<div class="list-item-title">Aktiv rabat</div>
</div>
<div class="list-item-controls list-item-controls-force-visible">
<div class="tags has-addons">
<span class="tag is-success is-light"> -{{ highestEligibleDiscount }}%</span>
</div>
</div>
</a>
<!-- Product discount -->
<a class="list-item">
<div class="list-item-content">
<div class="list-item-title">Produkt rabat</div>
</div>
<div class="list-item-controls list-item-controls-force-visible">
<div class="tags has-addons">
<span class="tag is-warning is-light"> -{{ discount_product }}%</span>
<span class="tag is-light is-success" v-if="isCategoryDiscountsEnabled">Tilladt</span>
<span class="tag is-light is-danger" v-else>Ikke tilladt</span>
</div>
</div>
</a>
<!-- Category discount -->
<a class="list-item">
<div class="list-item-content">
<div class="list-item-title">Kategori rabat</div>
</div>
<div class="list-item-controls list-item-controls-force-visible">
<div class="tags has-addons">
<span class="tag is-warning is-light"> -{{ discount_category }}%</span>
<span class="tag is-light is-success" v-if="isCategoryDiscountsEnabled">Tilladt</span>
<span class="tag is-light is-danger" v-else>Ikke tilladt</span>
</div>
</div>
</a>
<!-- Global discount -->
<a class="list-item">
<div class="list-item-content">
<div class="list-item-title">E-conomic rabat</div>
</div>
<div class="list-item-controls list-item-controls-force-visible">
<div class="tags has-addons">
<span class="tag is-warning is-light"> -{{ discount_global }}%</span>
<span class="tag is-light is-success" v-if="isGlobalDiscountsEnabled">Tilladt</span>
<span class="tag is-light is-danger" v-else>Ikke tilladt</span>
</div>
</div>
</a>
</div>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.list-item-controls-force-visible {
opacity: initial!important;
visibility: initial!important;
}
.text-can-not-select {
user-select: none;
}
</style>
@@ -461,13 +461,6 @@ onMounted(() => {
font-size: 0.8rem;
">
{{ category.name }}
{{
user_discounts !== undefined
?getUserGlobalDiscount() !== 0
? `( -${getUserGlobalDiscount() }% )`
: ''
: ''
}}
</a>
</li>
</ul>
@@ -586,6 +579,7 @@ onMounted(() => {
:class="{ 'is-selected': selectedProduct === product.id }"
>
<ProductBox
:product="product"
:id="product.id"
:name="product.name"
:description="product.description"
@@ -602,6 +596,7 @@ onMounted(() => {
@addToCart="addProductWithAddonsToOrder(product.id)"
:unselect="unselectProduct"
:setProductAddonNote="setProductAddonNote"
:customer-discounts="user_discounts"
/>
</WhiteBox>
</div>
@@ -128,7 +128,7 @@ export const getUserCategoryDiscount = (category) => {
try {
categoryDiscount = parseInt(user.discounts.value.find(
(discount) =>
parseInt(discount.product_or_category_id) === category && parseInt(discount.is_category) === 1
parseInt(discount.product_or_category_id) === category && discount.is_category
).percentage) || null;
} catch (e) {}
return categoryDiscount ? categoryDiscount : 0;
@@ -140,7 +140,7 @@ export const getUserGlobalDiscount = () => {
try {
globalDiscount = parseInt(user.discounts.value.find(
(discount) =>
discount.id === '999999'
discount.id === 999999
).percentage) || null;
} catch (e) {}
return globalDiscount ? globalDiscount : 0;
@@ -152,7 +152,7 @@ export const getUserProductOnlyDiscount = (product) => {
// Find the discount for the product
let discount = null;
try {
discount = user.discounts.value.find(discount => discount.product_or_category_id == productId && discount.is_category == 0) || null;
discount = user.discounts.value.find(discount => discount.product_or_category_id == productId && !discount.is_category) || null;
} catch (e) {}
// If the discount is not found, return 0
return discount ? discount.percentage : 0;
@@ -90,20 +90,20 @@ const editDiscount = (product, isCategory) => {
<!-- Discount: Item -->
<td
class="is-clickable"
@click="editDiscount(product, 0)"
@click="editDiscount(product, false)"
v-if="getUserProductOnlyDiscount(product) > 0"
>{{ getUserProductOnlyDiscount(product) }} %
<i class="is-pulled-right fas fa-edit"></i>
</td>
<td
class="is-clickable"
@click="editDiscount(product, 0)"
@click="editDiscount(product, false)"
v-else>- <i class="is-pulled-right fas fa-edit"></i>
</td>
<!-- Discount: Category -->
<td
class="is-clickable"
@click="editDiscount(product, 1)"
@click="editDiscount(product, true)"
v-if="getUserCategoryDiscount(getProductCategory(product)) > 0">{{ getUserCategoryDiscount(getProductCategory(product)) }} % {{ isProductAllowedToApplyCategoryDiscounts(product) ? ' ' : ' - Disabled for item. ' }}
<i class="is-pulled-right fas fa-edit"></i>
</td>