add price into product
This commit is contained in:
@@ -12,7 +12,9 @@
|
||||
:mode="props.mode"
|
||||
:style="props.style"
|
||||
:multiple="props.multiple"
|
||||
:dropdown-style="dropdownStyle"
|
||||
:dropdown-style="mergedDropdownStyle"
|
||||
:popup-class-name="props.popupClassName"
|
||||
:dropdown-match-select-width="props.dropdownMatchSelectWidth"
|
||||
@change="handleChange"
|
||||
:placeholder="props.placeholder"
|
||||
:allowClear="props.allowClear"
|
||||
@@ -60,13 +62,22 @@ interface Prop {
|
||||
allowClear?: boolean
|
||||
hasTooltip?: boolean
|
||||
noData?: string
|
||||
popupClassName?: string
|
||||
dropdownMatchSelectWidth?: boolean
|
||||
dropdownStyle?: Record<string, string | number>
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Prop>(), {
|
||||
allowClear: true,
|
||||
hasTooltip: true
|
||||
hasTooltip: true,
|
||||
dropdownMatchSelectWidth: true
|
||||
})
|
||||
|
||||
const mergedDropdownStyle = computed(() => ({
|
||||
...dropdownStyle,
|
||||
...(props.dropdownStyle ?? {})
|
||||
}))
|
||||
|
||||
// console.log('props.items ====>', props.items)
|
||||
//define emits
|
||||
const emit = defineEmits(['update:value', 'change'])
|
||||
|
||||
@@ -43,7 +43,8 @@ export interface Product{
|
||||
}
|
||||
|
||||
export interface MethodePrices {
|
||||
id: string
|
||||
key?: string
|
||||
id?: string
|
||||
name: string
|
||||
price: number
|
||||
profit: number
|
||||
|
||||
@@ -18,10 +18,21 @@
|
||||
<span>قیمت ثابت :</span>
|
||||
<BasicInput class="md:w-[20rem]" addon-before="ریال" type="price" v-model:value="form.price" />
|
||||
</div>
|
||||
<div v-if="meltedPriceOption === 'reference'" class="flex flex-col gap-2">
|
||||
<div class="md:flex justify-between items-center">
|
||||
<div class="ml-4">مرجع قیمت :</div>
|
||||
<BasicSelect class="w-11rem method_style" allowClear :items="pricesList" valueName="name" keyName="id" v-model:value="form.reading_method" />
|
||||
<div v-if="meltedPriceOption === 'reference'" class="flex flex-col gap-2 md:min-w-[28rem]">
|
||||
<div class="flex flex-col gap-1">
|
||||
<div>مرجع قیمت :</div>
|
||||
<BasicSelect
|
||||
class="price-reference-select"
|
||||
allowClear
|
||||
:hasTooltip="false"
|
||||
popup-class-name="price-reference-select-dropdown"
|
||||
:dropdown-match-select-width="false"
|
||||
:dropdown-style="{ minWidth: '28rem' }"
|
||||
:items="pricesList"
|
||||
valueName="name"
|
||||
keyName="id"
|
||||
v-model:value="form.reading_method"
|
||||
/>
|
||||
</div>
|
||||
<div class="md:flex justify-between items-center">
|
||||
<div class="ml-4">تعدیل نسبت به قیمت مرجع (٪) :</div>
|
||||
@@ -74,10 +85,21 @@
|
||||
<template #icon><Icon icon="vuesax-linear:minus" size="20" /></template>
|
||||
</AButton>
|
||||
</div>
|
||||
<div v-if="methodOption === '2'" class="flex flex-col gap-2">
|
||||
<div class="md:flex justify-between items-center">
|
||||
<div class="ml-4">قیمت :</div>
|
||||
<BasicSelect class="w-11rem method_style" allowClear :items="pricesList" valueName="name" keyName="id" v-model:value="form.reading_method" />
|
||||
<div v-if="methodOption === '2'" class="flex flex-col gap-2 md:min-w-[28rem]">
|
||||
<div class="flex flex-col gap-1">
|
||||
<div>قیمت :</div>
|
||||
<BasicSelect
|
||||
class="price-reference-select"
|
||||
allowClear
|
||||
:hasTooltip="false"
|
||||
popup-class-name="price-reference-select-dropdown"
|
||||
:dropdown-match-select-width="false"
|
||||
:dropdown-style="{ minWidth: '28rem' }"
|
||||
:items="pricesList"
|
||||
valueName="name"
|
||||
keyName="id"
|
||||
v-model:value="form.reading_method"
|
||||
/>
|
||||
</div>
|
||||
<div class="md:flex justify-between items-center">
|
||||
<div class="ml-4">تعدیل نسبت به قیمت مرجع (٪) :</div>
|
||||
@@ -95,6 +117,30 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-2 rounded-lg border border-solid border-blue-200 bg-blue-50 p-3">
|
||||
<div class="flex flex-wrap items-center justify-between gap-2">
|
||||
<div class="text-sm font-600 text-gray-700">قیمت لحظهای</div>
|
||||
<AButton size="small" :loading="pricesLoading || appPriceLoading" @click="$emit('refreshPrices')">
|
||||
بروزرسانی
|
||||
</AButton>
|
||||
</div>
|
||||
<div v-if="livePricePreview.status === 'ready'" class="mt-2 text-lg font-700 text-blue-700">
|
||||
{{ usePrice(livePricePreview.amount) }}
|
||||
</div>
|
||||
<div v-else-if="livePricePreview.status === 'loading'" class="mt-2 text-sm text-gray-500">
|
||||
{{ livePricePreview.message }}
|
||||
</div>
|
||||
<div v-else class="mt-2 text-sm text-gray-500">
|
||||
{{ livePricePreview.message }}
|
||||
</div>
|
||||
<div v-if="livePricePreview.status === 'ready'" class="mt-1 text-xs text-gray-500">
|
||||
مبنا: {{ livePricePreview.source }}
|
||||
<span v-if="Number(form.price_offset_percent) !== 0">
|
||||
— تعدیل {{ form.price_offset_percent }}٪
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="!isMeltedCategory"
|
||||
class="md:flex justify-between p-2 border-1 border-solid rounded-lg border-gray-200 mt-2"
|
||||
@@ -183,7 +229,11 @@ import Icon from '@core/components/BaseIcon/src/Icon.vue'
|
||||
import BaseCollapse from '@/components/BaseCollapse.vue'
|
||||
import IndexImageUploader from '@/components/ProductUploader/IndexImageUploader.vue'
|
||||
import { getConfiguration } from '@/api/configuration'
|
||||
import type { last } from '@/model/price'
|
||||
import type { MethodePrices } from '@/model/product'
|
||||
import { usePrice } from '@core/utils/usePrice'
|
||||
import { isMeltedTradeGroup } from '@/utils/productCategories'
|
||||
import { computeProductEditorPricePreview } from '@/utils/productEditorPrice'
|
||||
import { resolveTradeWeightUnit, weightFieldLabelForUnit } from '@/utils/weightUnit'
|
||||
import dayjs from 'dayjs'
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
@@ -196,6 +246,10 @@ const props = defineProps<{
|
||||
meltedPriceOption: string
|
||||
activeKey: string[]
|
||||
pricesList: Array<{ id: string; name: string }>
|
||||
pricesData: MethodePrices[]
|
||||
pricesLoading: boolean
|
||||
appLastPrice: (last & { price?: number }) | null
|
||||
appPriceLoading: boolean
|
||||
categoryItems: any[]
|
||||
metalItems: any[]
|
||||
priceItems: any[]
|
||||
@@ -208,6 +262,18 @@ const props = defineProps<{
|
||||
const tradeWeightUnit = ref('گرم')
|
||||
const isMeltedCategory = computed(() => isMeltedTradeGroup(props.form.group_type))
|
||||
|
||||
const livePricePreview = computed(() =>
|
||||
computeProductEditorPricePreview({
|
||||
form: props.form,
|
||||
methodOption: props.methodOption,
|
||||
meltedPriceOption: props.meltedPriceOption as 'app' | 'fixed' | 'reference',
|
||||
pricesData: props.pricesData,
|
||||
appLastPrice: props.appLastPrice,
|
||||
pricesLoading: props.pricesLoading,
|
||||
appPriceLoading: props.appPriceLoading
|
||||
})
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.meltedPriceOption,
|
||||
(option) => {
|
||||
@@ -263,5 +329,34 @@ defineEmits<{
|
||||
(e: 'update:productIndexImage', value: any[]): void
|
||||
(e: 'uploadedIndexFile', id: number, response?: Record<string, any>): void
|
||||
(e: 'deleteIndexFile'): void
|
||||
(e: 'refreshPrices'): void
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.price-reference-select {
|
||||
width: 100%;
|
||||
min-width: 22rem;
|
||||
}
|
||||
|
||||
.price-reference-select :deep(.ant-select-selector) {
|
||||
height: auto !important;
|
||||
min-height: 32px;
|
||||
}
|
||||
|
||||
.price-reference-select :deep(.ant-select-selection-item) {
|
||||
white-space: normal;
|
||||
line-height: 1.4;
|
||||
overflow: visible;
|
||||
text-overflow: unset;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
.price-reference-select-dropdown .ant-select-item-option-content {
|
||||
white-space: normal;
|
||||
overflow: visible;
|
||||
text-overflow: unset;
|
||||
line-height: 1.4;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -23,6 +23,10 @@
|
||||
:weightOption="weightOption"
|
||||
:activeKey="activeKey_"
|
||||
:pricesList="pricesList"
|
||||
:pricesData="pricesData"
|
||||
:pricesLoading="pricesLoading"
|
||||
:appLastPrice="appLastPrice"
|
||||
:appPriceLoading="appPriceLoading"
|
||||
:categoryItems="categorySelectItems"
|
||||
:metalItems="metalItems"
|
||||
:priceItems="priceItems"
|
||||
@@ -40,6 +44,7 @@
|
||||
@minusPrice="minusPrice"
|
||||
@uploadedIndexFile="uploadedIndexFile"
|
||||
@deleteIndexFile="deleteIndexFile"
|
||||
@refreshPrices="refreshLivePrices"
|
||||
/>
|
||||
</BasicActionSheet>
|
||||
</div>
|
||||
@@ -52,10 +57,11 @@ import locale from 'ant-design-vue/es/date-picker/locale/fa_IR'
|
||||
import { cloneDeep } from 'lodash'
|
||||
import dayjs from 'dayjs'
|
||||
import customParseFormat from 'dayjs/plugin/customParseFormat'
|
||||
import { computed, nextTick, onMounted, ref } from 'vue'
|
||||
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import { createProduct, getCustomerById, updateCustomer } from '@/api/product'
|
||||
import { fetchProductImageUrl } from '@/api/file'
|
||||
import { methodePrices } from '@/api/price'
|
||||
import { getLastPrice, methodePrices } from '@/api/price'
|
||||
import type { last } from '@/model/price'
|
||||
import BasicActionSheet from '@/components/BasicActionSheet.vue'
|
||||
import type { Item } from '@/model/Item'
|
||||
import type { MethodePrices, Product } from '@/model/product'
|
||||
@@ -123,23 +129,74 @@ const priceItems: Array<Item> = [
|
||||
]
|
||||
|
||||
const pricesData = ref<MethodePrices[]>([])
|
||||
const pricesLoading = ref(false)
|
||||
const appLastPrice = ref<(last & { price?: number }) | null>(null)
|
||||
const appPriceLoading = ref(false)
|
||||
let pricePollTimer: ReturnType<typeof setInterval> | undefined
|
||||
|
||||
const pricesList = computed(() =>
|
||||
pricesData.value.map((x) => ({
|
||||
id: `${x.name}`,
|
||||
name: `${x.name}(${usePrice(x.price)})`
|
||||
id: `${x.key ?? x.id ?? x.name}`,
|
||||
name: `${x.name} (${usePrice(x.price)})`
|
||||
}))
|
||||
)
|
||||
|
||||
async function fetchPrices() {
|
||||
pricesLoading.value = true
|
||||
try {
|
||||
pricesData.value = await methodePrices()
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
} finally {
|
||||
pricesLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchAppPrice() {
|
||||
appPriceLoading.value = true
|
||||
try {
|
||||
appLastPrice.value = await getLastPrice()
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
appLastPrice.value = null
|
||||
} finally {
|
||||
appPriceLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshLivePrices() {
|
||||
await Promise.all([fetchPrices(), fetchAppPrice()])
|
||||
}
|
||||
|
||||
function startPricePolling() {
|
||||
stopPricePolling()
|
||||
void refreshLivePrices()
|
||||
pricePollTimer = setInterval(() => {
|
||||
void refreshLivePrices()
|
||||
}, 10000)
|
||||
}
|
||||
|
||||
function stopPricePolling() {
|
||||
if (pricePollTimer) {
|
||||
clearInterval(pricePollTimer)
|
||||
pricePollTimer = undefined
|
||||
}
|
||||
}
|
||||
|
||||
watch(addUserVisible, (visible) => {
|
||||
if (visible) {
|
||||
startPricePolling()
|
||||
} else {
|
||||
stopPricePolling()
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
fetchPrices()
|
||||
void refreshLivePrices()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
stopPricePolling()
|
||||
})
|
||||
|
||||
function uploadedIndexFile(id: number, response?: Record<string, any>) {
|
||||
@@ -372,8 +429,3 @@ const minusPrice = () => {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.method_style .ant-select-selector {
|
||||
width: 15rem !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
import type { last } from '@/model/price'
|
||||
import type { MethodePrices } from '@/model/product'
|
||||
import type { MeltedPriceSource } from '@/utils/productCategories'
|
||||
import { isMeltedTradeGroup } from '@/utils/productCategories'
|
||||
|
||||
const GOLD_DOT_REFERENCE_NAME = 'گلد دات ای از آبشده - رسمی - نقد فردا'
|
||||
|
||||
export function findMethodPriceEntry(
|
||||
prices: MethodePrices[],
|
||||
readingMethod?: string | null
|
||||
): MethodePrices | undefined {
|
||||
if (!readingMethod) return undefined
|
||||
|
||||
return prices.find(
|
||||
(entry) =>
|
||||
entry.key === readingMethod ||
|
||||
entry.id === readingMethod ||
|
||||
entry.name === readingMethod
|
||||
)
|
||||
}
|
||||
|
||||
export function normalizeReferenceBase(entry: MethodePrices): { base: number; profit: number } {
|
||||
let base = Number(entry.price) || 0
|
||||
const profit = Number(entry.profit) || 0
|
||||
|
||||
if (entry.name === GOLD_DOT_REFERENCE_NAME) {
|
||||
base = base / 4.3318
|
||||
}
|
||||
|
||||
return { base, profit }
|
||||
}
|
||||
|
||||
export function adjustedProductPrice(
|
||||
basePrice: number,
|
||||
baseProfit: number,
|
||||
offsetPercent: number
|
||||
): number {
|
||||
const reference = basePrice + baseProfit
|
||||
return Math.round(reference * (1 + (Number(offsetPercent) || 0) / 100))
|
||||
}
|
||||
|
||||
export function resolveAppBasePrice(lastPrice?: Partial<last> & { price?: number } | null): number {
|
||||
if (!lastPrice) return 0
|
||||
|
||||
const rawPrice = Number(lastPrice.price)
|
||||
if (rawPrice > 0) return rawPrice
|
||||
|
||||
const buy = Number(lastPrice.buy) || 0
|
||||
const sell = Number(lastPrice.sell) || 0
|
||||
if (buy > 0 && sell > 0) return Math.round((buy + sell) / 2)
|
||||
|
||||
return Number(lastPrice.price_last) || 0
|
||||
}
|
||||
|
||||
export type ProductEditorPricePreview =
|
||||
| { status: 'ready'; amount: number; source: string }
|
||||
| { status: 'empty'; message: string }
|
||||
| { status: 'loading'; message: string }
|
||||
|
||||
export function computeProductEditorPricePreview(input: {
|
||||
form: {
|
||||
group_type?: string
|
||||
price?: number | null
|
||||
reading_method?: string | null
|
||||
price_offset_percent?: number | null
|
||||
}
|
||||
methodOption: string
|
||||
meltedPriceOption: MeltedPriceSource
|
||||
pricesData: MethodePrices[]
|
||||
appLastPrice?: (Partial<last> & { price?: number }) | null
|
||||
pricesLoading?: boolean
|
||||
appPriceLoading?: boolean
|
||||
}): ProductEditorPricePreview {
|
||||
const offsetPercent = Number(input.form.price_offset_percent ?? 0)
|
||||
const melted = isMeltedTradeGroup(input.form.group_type)
|
||||
|
||||
if (melted) {
|
||||
if (input.meltedPriceOption === 'fixed') {
|
||||
const amount = Number(input.form.price) || 0
|
||||
return amount > 0
|
||||
? { status: 'ready', amount, source: 'قیمت ثابت' }
|
||||
: { status: 'empty', message: 'قیمت ثابت را وارد کنید' }
|
||||
}
|
||||
|
||||
if (input.meltedPriceOption === 'app') {
|
||||
if (input.appPriceLoading) {
|
||||
return { status: 'loading', message: 'در حال دریافت قیمت اپ...' }
|
||||
}
|
||||
const base = resolveAppBasePrice(input.appLastPrice)
|
||||
if (base <= 0) {
|
||||
return { status: 'empty', message: 'قیمت اپ در دسترس نیست' }
|
||||
}
|
||||
return {
|
||||
status: 'ready',
|
||||
amount: adjustedProductPrice(base, 0, offsetPercent),
|
||||
source: 'قیمت صفحه اصلی اپ'
|
||||
}
|
||||
}
|
||||
|
||||
if (!input.form.reading_method) {
|
||||
return { status: 'empty', message: 'مرجع قیمت را انتخاب کنید' }
|
||||
}
|
||||
if (input.pricesLoading) {
|
||||
return { status: 'loading', message: 'در حال دریافت قیمت مرجع...' }
|
||||
}
|
||||
const entry = findMethodPriceEntry(input.pricesData, input.form.reading_method)
|
||||
if (!entry) {
|
||||
return { status: 'empty', message: 'مرجع قیمت یافت نشد' }
|
||||
}
|
||||
const { base, profit } = normalizeReferenceBase(entry)
|
||||
return {
|
||||
status: 'ready',
|
||||
amount: adjustedProductPrice(base, profit, offsetPercent),
|
||||
source: entry.name
|
||||
}
|
||||
}
|
||||
|
||||
if (input.methodOption === '1') {
|
||||
const amount = Number(input.form.price) || 0
|
||||
return amount > 0
|
||||
? { status: 'ready', amount, source: 'قیمت دستی' }
|
||||
: { status: 'empty', message: 'قیمت وسط را وارد کنید' }
|
||||
}
|
||||
|
||||
if (!input.form.reading_method) {
|
||||
return { status: 'empty', message: 'مرجع قیمت را انتخاب کنید' }
|
||||
}
|
||||
if (input.pricesLoading) {
|
||||
return { status: 'loading', message: 'در حال دریافت قیمت مرجع...' }
|
||||
}
|
||||
const entry = findMethodPriceEntry(input.pricesData, input.form.reading_method)
|
||||
if (!entry) {
|
||||
return { status: 'empty', message: 'مرجع قیمت یافت نشد' }
|
||||
}
|
||||
const { base, profit } = normalizeReferenceBase(entry)
|
||||
return {
|
||||
status: 'ready',
|
||||
amount: adjustedProductPrice(base, profit, offsetPercent),
|
||||
source: entry.name
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user