fix bugs of physicalRecieve & login-code-input & odometer-digit

This commit is contained in:
2026-07-20 16:57:03 +03:30
parent cedd1ae586
commit a60f1fbc68
15 changed files with 545 additions and 73 deletions
@@ -36,11 +36,10 @@
/>
<span class="price-change-badge__value">{{ formatSignedPercentChange(percentChange) }}</span>
</div>
<Vue3Odometer
:value="displayGoldVal"
<span
class="o-text !text-xl font-bold price-odometer--boursecu"
style="direction: ltr"
/>
>{{ usePriceWithoutType(displayGoldVal) }}</span>
<span class="price-unit--boursecu">ریال</span>
</div>
</div>
@@ -59,9 +58,7 @@ import {
getPriceDisplayState,
getStoreClosedHint,
} from '@/utils/storeHours'
import Vue3Odometer from 'vue3-odometer'
import 'odometer/themes/odometer-theme-default.css'
import { formatSignedPercentChange } from '@/utils/usePrice'
import { formatSignedPercentChange, usePriceWithoutType } from '@/utils/usePrice'
const userStore = useUserStore()
@@ -36,10 +36,10 @@
/>
<span class="dhome-hero-price__delta-value">{{ formatSignedPercentChange(percentChange) }}</span>
</span>
<Vue3Odometer
:value="displayGoldVal"
<span
class="dhome-hero-price__odometer o-text dhome-hero-price__odometer--boursecu"
/>
style="direction: ltr"
>{{ usePriceWithoutType(displayGoldVal) }}</span>
<span class="dhome-hero-price__unit dhome-hero-price__unit--boursecu">ریال</span>
</div>
</template>
@@ -48,8 +48,6 @@
<script setup lang="ts">
import { computed } from 'vue'
import Vue3Odometer from 'vue3-odometer'
import 'odometer/themes/odometer-theme-default.css'
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
import { useUserStore } from '@/store/userStore'
import { getTradeBuyPrice, lastPrice, percentPriceChanged } from '@/utils/price'
@@ -59,7 +57,7 @@ import {
getPriceDisplayState,
getStoreClosedHint,
} from '@/utils/storeHours'
import { formatSignedPercentChange } from '@/utils/usePrice'
import { formatSignedPercentChange, usePriceWithoutType } from '@/utils/usePrice'
const userStore = useUserStore()
@@ -15,8 +15,8 @@
/>
<div class="pr-cart-row__stock text-xs">
<span>مقدار موجودی</span>
<span class="font-bold">{{ item.product.wallet.gold }}</span>
<span>{{ getProductQuantityUnit(item.product) }}</span>
<span class="font-bold">{{ displayGold }}</span>
<span>{{ quantityUnit }}</span>
<div
v-if="getProductUnitWeightLabel(item.product)"
class="mt-0.5 text-xs text-gray-500"
@@ -26,34 +26,42 @@
</div>
</div>
<div class="mt-2 flex justify-between items-center gap-2">
<div class="shrink-0">تعداد درخواستی:</div>
<div class="shrink-0">{{ isCountBased ? 'تعداد درخواستی:' : 'مقدار درخواستی:' }}</div>
<div
class="flex items-center gap-1 "
class="flex items-center gap-1"
:class="variant === 'desktop' ? 'dpr-cart-row__input' : 'pr-cart-row__input'"
>
<BasicInput
v-model:value="item.count"
:addon-before="getProductQuantityUnit(item.product)"
:addon-before="quantityUnit"
:addon-bg-color="config.config?.color"
addon-text-color="white"
type="text"
inputmode="numeric"
:inputmode="isCountBased ? 'numeric' : 'decimal'"
@update:value="onCountInput"
/>
</div>
</div>
<div v-if="countError" class="mt-1 text-xs text-red-600">{{ countError }}</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { computed, ref, watch } from 'vue'
import BasicButton from '@/components/BasicPacks/BasicButton.vue'
import BasicInput from '@/components/BasicPacks/BasicInput.vue'
import { useConfigStore } from '@/store/configStore'
import type { CountPInventory } from '@/models/products'
import { getProductDisplayImage } from '@/utils/productAssets'
import { getProductQuantityUnit, getProductUnitWeightLabel } from '@/utils/productDealMethod'
import {
getProductQuantityUnit,
getProductUnitWeightLabel,
isCountBasedProduct,
} from '@/utils/productDealMethod'
import { formatGoldAmount, usePriceWithoutType } from '@/utils/usePrice'
import { persianToEnglishDigits } from '@/utils/persianDigits'
interface Props {
item: CountPInventory
@@ -64,6 +72,10 @@ const props = withDefaults(defineProps<Props>(), {
variant: 'mobile',
})
const countError = ref('')
const isCountBased = computed(() => isCountBasedProduct(props.item.product))
const quantityUnit = computed(() => getProductQuantityUnit(props.item.product))
const displayImage = computed(() =>
getProductDisplayImage({
group_type: props.item.product.group_type,
@@ -72,8 +84,49 @@ const displayImage = computed(() =>
})
)
const displayGold = computed(() => {
const gold = Number(props.item.product.wallet?.gold ?? 0)
return isCountBased.value ? usePriceWithoutType(gold, false) : formatGoldAmount(gold)
})
const config = useConfigStore()
function onCountInput(raw: string | number) {
countError.value = ''
const normalized = persianToEnglishDigits(String(raw ?? '').trim())
if (!normalized) {
props.item.count = 0 as any
return
}
const numeric = Number(normalized)
if (!Number.isFinite(numeric)) {
countError.value = 'مقدار معتبر وارد کنید'
return
}
if (isCountBased.value) {
if (numeric % 1 !== 0) {
countError.value = 'برای محصول تعدادی فقط عدد صحیح مجاز است'
props.item.count = Math.floor(numeric) as any
return
}
props.item.count = Math.floor(numeric) as any
}
}
watch(
() => props.item.count,
(value) => {
if (!isCountBased.value) return
const numeric = Number(value)
if (Number.isFinite(numeric) && numeric % 1 !== 0) {
props.item.count = Math.floor(numeric) as any
countError.value = 'برای محصول تعدادی فقط عدد صحیح مجاز است'
}
}
)
const emit = defineEmits<{
remove: [id: number]
}>()
@@ -8,13 +8,28 @@ import { physicalReceiveApi } from '@/api/physicalreceive'
import { tableAddress } from '@/api/address'
import type { CountPInventory, PInventory } from '@/models/products'
import type { Address } from '@/models/address'
import type { WalletAssetsSummary } from '@/models/walletAssets'
import { loading } from '@/utils'
import { enrichInventoryWithCatalog } from '@/utils/productAssets'
import {
enrichInventoryWithCatalog,
getProductInventoryQuantity,
} from '@/utils/productAssets'
import { resolvePhysicalDeliveryState } from '@/utils/physicalDelivery'
import { isCountBasedProduct } from '@/utils/productDealMethod'
import { useWalletAssets } from '@/composables/useWalletAssets'
import { useWalletStore } from '@/store/walletStore'
import { usePhysicalReceiveShellContext } from './usePhysicalReceiveShell'
export interface PendingDeliveryItem {
id: number
title: string
message: string
}
export interface NewPhysicalRequestContext {
isDesktop: Ref<boolean>
items: Ref<PInventory[] | undefined>
pendingDeliveryItems: Ref<PendingDeliveryItem[]>
selectedProduct: Ref<number | undefined>
selectedProducts: Ref<CountPInventory[]>
address: Ref<Address[] | undefined>
@@ -31,11 +46,37 @@ export interface NewPhysicalRequestContext {
export const NEW_PHYSICAL_REQUEST_KEY: InjectionKey<NewPhysicalRequestContext> =
Symbol('newPhysicalRequest')
function syncInventoryWithWallet(
products: PInventory[],
summary: WalletAssetsSummary | null | undefined,
mainWalletGold?: number | null
): PInventory[] {
const inventoryCards = summary?.inventory_cards ?? []
return products.map((product) => {
const gold = getProductInventoryQuantity(inventoryCards, product.id, mainWalletGold)
if (!product.wallet) {
return product
}
return {
...product,
wallet: {
...product.wallet,
gold,
},
}
})
}
export function useNewPhysicalRequestPage() {
const { isDesktop } = usePhysicalReceiveShellContext()
const { loadWalletAssets, assetsSummary } = useWalletAssets()
const walletStore = useWalletStore()
const items = ref<PInventory[]>()
const baseItems = ref<PInventory[]>()
const pendingDeliveryItems = ref<PendingDeliveryItem[]>([])
const selectedAddressID = ref<number>()
const selectedProduct = ref<number>()
const productsByWalletId = ref<Array<{ wallet_id: number; value: number }>>([])
@@ -76,10 +117,37 @@ export function useNewPhysicalRequestPage() {
const [inventory, catalog] = await Promise.all([
getProductInventory(),
getProductsCatalog(),
loadWalletAssets(),
])
const enriched = enrichInventoryWithCatalog(inventory.rows ?? [], catalog ?? [])
items.value = enriched
baseItems.value = cloneDeep(enriched)
const withBalance = syncInventoryWithWallet(
enrichInventoryWithCatalog(inventory.rows ?? [], catalog ?? []),
assetsSummary.value,
walletStore.walletInfo?.gold
).filter(
(product) =>
product.title !== 'آبشده نقدی' && Number(product.wallet?.gold ?? 0) > 0
)
// فقط محصولاتی که تحویل فیزیکی دارند و به حداقل گرم رسیده‌اند قابل انتخاب‌اند.
// محصولاتی که تحویل دارند ولی هنوز به حداقل نرسیده‌اند به‌صورت پیام نمایش داده می‌شوند.
const eligible: PInventory[] = []
const pending: PendingDeliveryItem[] = []
for (const product of withBalance) {
const state = resolvePhysicalDeliveryState(product as any)
if (state.status === 'ready') {
eligible.push(product)
} else if (state.status === 'below_min') {
pending.push({
id: product.id,
title: product.title ?? 'محصول',
message: `${product.title ?? 'محصول'}: ${state.message}`,
})
}
}
items.value = eligible
baseItems.value = cloneDeep(eligible)
pendingDeliveryItems.value = pending
} catch (e) {
console.log(e)
} finally {
@@ -91,10 +159,21 @@ export function useNewPhysicalRequestPage() {
try {
const zeroAmount = selectedProducts.value.map((item) => item.count <= 0)
if (zeroAmount.includes(true)) {
message.error('لطفاً تعداد درخواستی محصول را وارد کنید.')
} else {
showAddress.value = true
message.error('لطفاً مقدار درخواستی محصول را وارد کنید.')
return
}
const invalidCount = selectedProducts.value.find((item) => {
if (!isCountBasedProduct(item.product)) return false
const value = Number(item.count)
return !Number.isFinite(value) || value % 1 !== 0
})
if (invalidCount) {
message.error(`برای «${invalidCount.product.title}» فقط تعداد صحیح مجاز است`)
return
}
showAddress.value = true
} catch (e) {
console.log(e)
}
@@ -145,6 +224,7 @@ export function useNewPhysicalRequestPage() {
const context: NewPhysicalRequestContext = {
isDesktop,
items,
pendingDeliveryItems,
selectedProduct,
selectedProducts,
address,
@@ -22,6 +22,17 @@
<p v-if="items?.length" class="dpr-new__hint">
{{ items.length }} محصول با موجودی قابل تحویل
</p>
<div v-if="pendingDeliveryItems.length" class="dpr-new__pending">
<p class="dpr-new__pending-title">نزدیک به حد تحویل فیزیکی</p>
<p
v-for="pending in pendingDeliveryItems"
:key="pending.id"
class="dpr-new__pending-row"
>
{{ pending.message }}
</p>
</div>
</section>
<section class="dpr-surface dpr-new__cart">
@@ -104,6 +115,7 @@ import { useNewPhysicalRequestContext } from '../composables/useNewPhysicalReque
const {
items,
pendingDeliveryItems,
selectedProduct,
selectedProducts,
address,
@@ -117,3 +129,26 @@ const {
handleAddress,
} = useNewPhysicalRequestContext()
</script>
<style scoped>
.dpr-new__pending {
margin-top: 0.75rem;
padding: 0.5rem 0.75rem;
border-radius: 0.5rem;
background: #fff7ed;
border: 1px solid #fed7aa;
}
.dpr-new__pending-title {
font-size: 0.8rem;
font-weight: 600;
color: #c2410c;
margin-bottom: 0.25rem;
}
.dpr-new__pending-row {
font-size: 0.78rem;
color: #9a3412;
line-height: 1.6;
}
</style>
@@ -17,6 +17,17 @@
افزودن
</BasicButton>
</div>
<div v-if="pendingDeliveryItems.length" class="physical-new-request__pending mx-3 mt-2">
<p class="physical-new-request__pending-title">نزدیک به حد تحویل فیزیکی</p>
<p
v-for="pending in pendingDeliveryItems"
:key="pending.id"
class="physical-new-request__pending-row"
>
{{ pending.message }}
</p>
</div>
</div>
<div class="physical-new-request__items px-3">
@@ -67,6 +78,7 @@ import { useNewPhysicalRequestContext } from '../composables/useNewPhysicalReque
const {
items,
pendingDeliveryItems,
selectedProduct,
selectedProducts,
address,
@@ -122,4 +134,24 @@ const {
background: #fff;
border-top: 1px solid #f3f4f6;
}
.physical-new-request__pending {
padding: 0.5rem 0.75rem;
border-radius: 0.5rem;
background: #fff7ed;
border: 1px solid #fed7aa;
}
.physical-new-request__pending-title {
font-size: 0.8rem;
font-weight: 600;
color: #c2410c;
margin-bottom: 0.25rem;
}
.physical-new-request__pending-row {
font-size: 0.78rem;
color: #9a3412;
line-height: 1.6;
}
</style>
@@ -118,6 +118,15 @@
<div>{{ productWalletBalanceText }}</div>
</div>
</Transition>
<Transition mode="out-in" name="fade">
<div
v-if="physicalDeliveryHint"
class="text-xs mt-1"
:class="physicalDeliveryState.status === 'ready' ? 'text-green-600' : 'text-orange-600'"
>
{{ physicalDeliveryHint }}
</div>
</Transition>
<Transition mode="out-in" name="fade">
<div v-if="priceErrorVisible" class="flex gap-1 items-center text-xs mt-[-15px] text-orange-600">
<BasicIcon icon="vuesax-linear:danger" />
@@ -321,6 +330,7 @@ import {
} from '@/utils/productOrderLimit'
import { useWalletAssets } from '@/composables/useWalletAssets'
import { formatProductWalletBalanceText, formatInventoryCardValue, getCashWalletBalance, getProductInventoryQuantity } from '@/utils/productAssets'
import { resolvePhysicalDeliveryState } from '@/utils/physicalDelivery'
import { isStoreClosed, resolveStoreClosedNotice } from '@/utils/storeHours'
const catalogProducts = getProductsCatalogRef()
@@ -371,6 +381,24 @@ const productWalletBalanceText = computed(() =>
)
)
const physicalDeliveryState = computed(() =>
resolvePhysicalDeliveryState(props?.ProductItem as any, productWalletBalance.value)
)
const physicalDeliveryHint = computed(() => {
if (!props?.ProductItem?.has_physical_delivery) {
return ''
}
const state = physicalDeliveryState.value
if (state.status === 'ready') {
return 'امکان تحویل فیزیکی این محصول فعال است'
}
if (state.status === 'below_min') {
return state.message
}
return ''
})
const infoVisible = ref(false)
const walletStore = useWalletStore()
const { assetsSummary, loadWalletAssets } = useWalletAssets()