diff --git a/src/components/displays/department/pos/displays/CustomerDiscountsDepartmentDisplay.vue b/src/components/displays/department/pos/displays/CustomerDiscountsDepartmentDisplay.vue index 6c7f53d2..6797183d 100644 --- a/src/components/displays/department/pos/displays/CustomerDiscountsDepartmentDisplay.vue +++ b/src/components/displays/department/pos/displays/CustomerDiscountsDepartmentDisplay.vue @@ -24,8 +24,24 @@ const onCustomerChange = () => { isCustomerSelected.value = false; } -const parseValue = (value) => { - let tmp_value = parseFloat(value).toFixed(2); +const parseFixedPriceValue = (value) => { + if (value === null || value === undefined || value === '') { + return null; + } + + const parsed = Number.parseInt(String(value), 10); + return Number.isFinite(parsed) ? parsed : null; +} + +const formatNumber = (value) => Number.isInteger(value) ? String(value) : value.toFixed(2); + +const parseValue = (discount) => { + const fixedPrice = parseFixedPriceValue(discount.fixed_price); + if (fixedPrice !== null) { + return `${formatNumber(fixedPrice)} Kr.`; + } + + let tmp_value = parseFloat(discount.percentage).toFixed(2); // Add the percentage sign return `${tmp_value}%`; } @@ -78,7 +94,7 @@ watch(customer_id, onCustomerChange, { immediate: true }); - {{ parseValue(discount.percentage) }} + {{ parseValue(discount) }} @@ -86,4 +102,4 @@ watch(customer_id, onCustomerChange, { immediate: true }); \ No newline at end of file + diff --git a/src/components/displays/department/pos/displays/CustomerProductDiscountDisplay.vue b/src/components/displays/department/pos/displays/CustomerProductDiscountDisplay.vue index edc7d689..1d8f44c9 100644 --- a/src/components/displays/department/pos/displays/CustomerProductDiscountDisplay.vue +++ b/src/components/displays/department/pos/displays/CustomerProductDiscountDisplay.vue @@ -29,6 +29,8 @@ const discount_product = ref(null); const discount_category = ref(null); // The global discount (if any) const discount_global = ref(null); +// The fixed product price (if any) +const fixed_product_price = ref(null); // Show the discounts dropdown const showDiscountsDropdown = ref(false); @@ -49,6 +51,19 @@ const hasGlobalDiscount = () => { return discount_global.value > 0; } +const hasFixedProductPrice = () => { + return fixed_product_price.value !== null; +} + +const parseFixedPriceValue = (value) => { + if (value === null || value === undefined || value === '') { + return null; + } + + const parsed = Number.parseInt(String(value), 10); + return Number.isFinite(parsed) ? parsed : null; +} + // Set the product discount const setProductDiscount = (percentage) => { // Check if the percentage is a number @@ -83,6 +98,16 @@ const setGlobalDiscount = (percentage) => { discount_global.value = percentage; } +const setFixedProductPrice = (fixedPrice) => { + const parsedFixedPrice = parseFixedPriceValue(fixedPrice); + if (parsedFixedPrice === null) { + fixed_product_price.value = null; + return; + } + + fixed_product_price.value = parsedFixedPrice; +} + // Debugging function const getDiscountDebug = () => { @@ -101,9 +126,15 @@ const getDiscountDebug = () => { // 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); + discount_product.value = null; + discount_category.value = null; + discount_global.value = null; + fixed_product_price.value = null; + + const customerDiscounts = Array.isArray(props.customer_discounts) ? props.customer_discounts : []; + const tmp_discount_product = customerDiscounts.find((discount) => discount.product_or_category_id == props.product.id && Number(discount.is_category) === 0) + const tmp_discount_category = customerDiscounts.find((discount) => discount.product_or_category_id == props.product.category && Number(discount.is_category) === 1); + const tmp_discount_global = customerDiscounts.find((discount) => discount.id === 999999 && Number(discount.is_category) === 1); //console.log("Product discount: ", tmp_discount_product); //console.log("Category discount: ", tmp_discount_category); @@ -112,6 +143,7 @@ const parseCustomerDiscounts = () => { // Set the product discount if (tmp_discount_product) { setProductDiscount(tmp_discount_product.percentage); + setFixedProductPrice(tmp_discount_product.fixed_price); } // Set the category discount if (tmp_discount_category) { @@ -125,6 +157,10 @@ const parseCustomerDiscounts = () => { // Get the best discount for the customer const getBestDiscount = () => { + if (hasFixedProductPrice()) { + highestEligibleDiscount.value = 0; + return 0; + } // Set the best discount to 0 let tmp_best_discount = 0; // Check if the product discount is higher than the current best discount @@ -182,13 +218,32 @@ watch(() => props.customer_discounts, () => { {{ fixed_product_price }} Kr. + -{{ highestEligibleDiscount }}% @@ -254,4 +310,4 @@ watch(() => props.customer_discounts, () => { .text-can-not-select { user-select: none; } - \ No newline at end of file + diff --git a/src/components/forms/department/pos/SelectProductsFormPOS.vue b/src/components/forms/department/pos/SelectProductsFormPOS.vue index dd9dec67..c9f62f86 100644 --- a/src/components/forms/department/pos/SelectProductsFormPOS.vue +++ b/src/components/forms/department/pos/SelectProductsFormPOS.vue @@ -6,7 +6,7 @@ import { loadOrderItems, productCategoryAllowed, setProductsCategory, - getUserProductDiscount, + getUserProductPrice, showFakeCreateOrderItem, department_id, user_discounts, @@ -549,10 +549,9 @@ const addPreviousOrderToCurrent = async (previousOrder) => { continue; } - const previousOrderProduct = getPreviousOrderProduct(orderItem); - const discount = getUserProductDiscount(previousOrderProduct || {}); + const previousOrderProduct = getPreviousOrderProduct(orderItem) || { id: productId, price: getPreviousOrderProductPrice(orderItem) }; const basePrice = getPreviousOrderProductPrice(orderItem); - const discountedPrice = (basePrice - (basePrice * (discount / 100))).toFixed(0); + const discountedPrice = String(getUserProductPrice({ ...previousOrderProduct, price: basePrice })); showFakeCreateOrderItem(productId, quantity, discountedPrice); await createOrderItem(orderId, productId, quantity); @@ -586,9 +585,7 @@ const getRecommendedProductPrice = (productId) => { return '0'; } - const discount = getUserProductDiscount(product); - const price = Number(product.price ?? 0); - return (price - (price * (discount / 100))).toFixed(0); + return String(getUserProductPrice(product)); }; const isRecommendedProductCategoryAllowed = (productId) => { diff --git a/src/components/shop/POSDepartmentProcess.vue b/src/components/shop/POSDepartmentProcess.vue index e9bf9be8..6a641140 100644 --- a/src/components/shop/POSDepartmentProcess.vue +++ b/src/components/shop/POSDepartmentProcess.vue @@ -1844,13 +1844,30 @@ export const getUserDiscounts = async () => { }); }; -/** Get user product discount */ -export const getUserProductDiscount = (product, allowCategory = null, onlyCategory = null) => { - // Check if the user discounts are loaded +const ensureUserDiscountsLoaded = () => { if (!has_user_discounts_loaded.value) { has_user_discounts_loaded.value = true; getUserDiscounts().then(() => {}); } +}; + +const isDirectCustomerProductDiscount = (discount, productId) => { + return discount?.product_or_category_id == productId && Number(discount?.is_category) === 0; +}; + +const parseFixedPriceValue = (value) => { + if (value === null || value === undefined || value === '') { + return null; + } + + const parsed = Number.parseInt(String(value), 10); + return Number.isFinite(parsed) ? parsed : null; +}; + +/** Get user product discount */ +export const getUserProductDiscount = (product, allowCategory = null, onlyCategory = null) => { + // Check if the user discounts are loaded + ensureUserDiscountsLoaded(); // Get the product's id const productId = product.id; @@ -1948,6 +1965,36 @@ export const getUserProductDiscount = (product, allowCategory = null, onlyCatego return appliedDiscount; }; +export const getUserProductFixedPrice = (product) => { + ensureUserDiscountsLoaded(); + + const productId = product?.id; + if (productId === null || productId === undefined) { + return null; + } + + const fixedPriceDiscount = (Array.isArray(user_discounts.value) ? user_discounts.value : []).find((discount) => ( + isDirectCustomerProductDiscount(discount, productId) && parseFixedPriceValue(discount.fixed_price) !== null + )); + + return fixedPriceDiscount ? parseFixedPriceValue(fixedPriceDiscount.fixed_price) : null; +}; + +export const getUserProductPrice = (product) => { + const fixedPrice = getUserProductFixedPrice(product); + if (fixedPrice !== null) { + return fixedPrice; + } + + const price = Number(product?.price ?? 0); + const discount = Number(getUserProductDiscount(product) || 0); + if (!Number.isFinite(price)) { + return 0; + } + + return Number((price - (price * (discount / 100))).toFixed(0)); +}; + export const showFakeCreateOrderItem = (product, quantity, price) => { order_items.value.push({ id: Math.floor(Math.random() * 1000), diff --git a/src/components/viewport/page/headers/menu/systemSearchSupport.ts b/src/components/viewport/page/headers/menu/systemSearchSupport.ts index f451f665..47ad381b 100644 --- a/src/components/viewport/page/headers/menu/systemSearchSupport.ts +++ b/src/components/viewport/page/headers/menu/systemSearchSupport.ts @@ -189,7 +189,7 @@ const descriptionKeyOverrides: Partial> = { xlvask_customers: ['email', 'city', 'address', 'vatnumber'], motorapi_lookups: ['endpoint', 'license_plate', 'result'], fxratesapi_conversion_rates: ['base', 'target', 'endpoint', 'result'], - customer_discounts: ['customer_number', 'percentage'], + customer_discounts: ['customer_number', 'fixed_price', 'percentage'], module_config: ['description', 'value'], department_goals: ['criteria.type', 'criteria.progress_alert_frequency', 'criteria.progress_alert_destination'] }; @@ -251,6 +251,7 @@ const keyFieldOverrides: Partial> = { ), customer_discounts: fieldList( field('Discount', 'discount', 'percentage'), + field('Fixed price', 'fixed_price'), field('Target', 'product_or_category_id', 'object_id'), field('Category', 'is_category'), field('Customer #', 'customer_number'), diff --git a/src/views/dashboards/superUserDashboard/user/SuperUserSelectedUserObject.vue b/src/views/dashboards/superUserDashboard/user/SuperUserSelectedUserObject.vue index 7749b619..28a4d523 100644 --- a/src/views/dashboards/superUserDashboard/user/SuperUserSelectedUserObject.vue +++ b/src/views/dashboards/superUserDashboard/user/SuperUserSelectedUserObject.vue @@ -61,6 +61,19 @@ export const getUserData = async () => { }); }; +const isDirectProductDiscount = (discount, productId) => { + return discount?.product_or_category_id == productId && Number(discount?.is_category) === 0; +}; + +const parseFixedPriceValue = (value) => { + if (value === null || value === undefined || value === '') { + return null; + } + + const parsed = Number.parseInt(String(value), 10); + return Number.isFinite(parsed) ? parsed : null; +}; + /** Get user product discount */ export const getUserProductDiscount = (product, allowCategory = 1, onlyCategory = 0, allowGlobal = 0) => { // Get the product id @@ -133,12 +146,25 @@ 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) || null; + discount = user.discounts.value.find(discount => isDirectProductDiscount(discount, productId)) || null; } catch (e) {} // If the discount is not found, return 0 return discount ? discount.percentage : 0; }; +export const getUserProductFixedPrice = (product) => { + const productId = product.id; + let fixedPriceDiscount = null; + + try { + fixedPriceDiscount = user.discounts.value.find((discount) => ( + isDirectProductDiscount(discount, productId) && parseFixedPriceValue(discount.fixed_price) !== null + )) || null; + } catch (e) {} + + return fixedPriceDiscount ? parseFixedPriceValue(fixedPriceDiscount.fixed_price) : null; +}; + export const isProductAllowedToApplyCategoryDiscounts = (product) => { // Check if the product is allowed to apply category discounts return parseInt(product.apply_category_discount) === 1; @@ -166,6 +192,21 @@ export const getProductBestApplicableDiscount = (product) => { return Math.max(productDiscount, categoryDiscount, globalDiscount); } +export const getProductEffectivePrice = (product) => { + const fixedPrice = getUserProductFixedPrice(product); + if (fixedPrice !== null) { + return fixedPrice; + } + + const basePrice = Number(product.price ?? 0); + const discount = Number(getProductBestApplicableDiscount(product) || 0); + if (!Number.isFinite(basePrice)) { + return 0; + } + + return Number((basePrice - (basePrice * (discount / 100))).toFixed(0)); +} + export const user = { id: ref(''), customer_number: ref(''), @@ -199,9 +240,11 @@ export const user = { getUserCategoryDiscount: getUserCategoryDiscount, getUserGlobalDiscount: getUserGlobalDiscount, getUserProductOnlyDiscount: getUserProductOnlyDiscount, + getUserProductFixedPrice: getUserProductFixedPrice, isProductAllowedToApplyCategoryDiscounts: isProductAllowedToApplyCategoryDiscounts, getProductCategory: getProductCategory, getProductBestApplicableDiscount: getProductBestApplicableDiscount, + getProductEffectivePrice: getProductEffectivePrice, }, }; diff --git a/src/views/dashboards/superUserDashboard/user/UserPricing.vue b/src/views/dashboards/superUserDashboard/user/UserPricing.vue index cab8ec91..322e74fa 100644 --- a/src/views/dashboards/superUserDashboard/user/UserPricing.vue +++ b/src/views/dashboards/superUserDashboard/user/UserPricing.vue @@ -3,7 +3,7 @@ import UserSubPageWrapper from "@/views/dashboards/superUserDashboard/user/UserSubPageWrapper.vue"; import PageTitle from "@/components/global/PageTitle.vue"; import { useRouter } from 'vue-router' -import { user, getUserData, userId, setUser, getUserProductDiscount, getUserGlobalDiscount, getProductBestApplicableDiscount, getUserProductOnlyDiscount, getProductCategory, getUserCategoryDiscount, isProductAllowedToApplyCategoryDiscounts } from "@/views/dashboards/superUserDashboard/user/SuperUserSelectedUserObject.vue"; +import { user, getUserData, userId, setUser, getUserProductDiscount, getUserGlobalDiscount, getProductBestApplicableDiscount, getUserProductOnlyDiscount, getUserProductFixedPrice, getProductEffectivePrice, getProductCategory, getUserCategoryDiscount, isProductAllowedToApplyCategoryDiscounts } from "@/views/dashboards/superUserDashboard/user/SuperUserSelectedUserObject.vue"; import { getProducts } from "@/components/shop/Products.vue"; import { ref } from 'vue'; import { authenticatedRequest } from "@/components/session/authenticatedRequest.vue"; @@ -56,6 +56,72 @@ const editDiscount = (product, isCategory) => { allowOutsideClick: () => !Swal.isLoading() }); } + +const formatPrice = (price) => `${Number(price || 0).toFixed(0)} Kr.`; + +const getProductPriceDisplay = (product) => { + const fixedPrice = getUserProductFixedPrice(product); + if (fixedPrice !== null) { + return `${formatPrice(fixedPrice)} (fast pris)`; + } + + const discount = getProductBestApplicableDiscount(product); + if (discount === 0) { + return formatPrice(product.price); + } + + return `${formatPrice(getProductEffectivePrice(product))} (${discount}% rabat)`; +}; + +const editFixedPrice = (product) => { + const fixedPrice = getUserProductFixedPrice(product); + Swal.fire({ + title: product.name + ' ( product: ' + product.id + ' )', + input: 'number', + inputValue: fixedPrice === null ? '' : fixedPrice, + inputLabel: 'Fast pris (Kr.)', + inputAttributes: { + autocapitalize: 'off', + min: 0, + step: 1 + }, + showCancelButton: true, + confirmButtonText: 'Save', + showLoaderOnConfirm: true, + inputValidator: (value) => { + const normalizedValue = String(value ?? '').trim(); + if (normalizedValue === '') { + return null; + } + + const parsedValue = Number(normalizedValue); + if (!Number.isInteger(parsedValue) || parsedValue < 0) { + return 'Fast pris skal være et heltal eller tom.'; + } + + return null; + }, + preConfirm: (value) => { + const normalizedValue = String(value ?? '').trim(); + const fixedPriceValue = normalizedValue === '' ? null : Number.parseInt(normalizedValue, 10); + return authenticatedRequest(`/superuser/user/discounts`, "POST", { + user_id: userId.value, + object_id: product.id, + discount: getUserProductOnlyDiscount(product), + fixed_price: fixedPriceValue, + is_category: false + }) + .then((response) => { + console.log(response); + getUserData(); + }) + .catch((error) => { + console.log(error); + }); + }, + allowOutsideClick: () => !Swal.isLoading() + }); +}