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
+115
View File
@@ -0,0 +1,115 @@
import type { PInventory, Product } from '@/models/products'
import { getProductQuantityUnit, isCountBasedProduct } from '@/utils/productDealMethod'
import { formatWeightAmount, usePriceWithoutType } from '@/utils/usePrice'
export type PhysicalDeliveryStatus = 'none' | 'ready' | 'below_min'
/** برچسب کوتاه کارت محصول: فقط «دارد/ندارد» تحویل فیزیکی — مستقل از موجودی کاربر */
export function getProductDeliveryBadge(
product: { has_physical_delivery?: boolean } | null | undefined
): { has: boolean; text: string } {
const has = Boolean(product?.has_physical_delivery)
return {
has,
text: has ? 'تحویل فیزیکی دارد' : 'تحویل فیزیکی ندارد',
}
}
export interface PhysicalDeliveryState {
status: PhysicalDeliveryStatus
available: number
min: number
remaining: number
unit: string
message: string
}
type DeliveryProduct = (Partial<Product> | Partial<PInventory>) & {
has_physical_delivery?: boolean
min_physical_delivery_quantity?: number | null
amount?: any
unit?: string | null
group_type?: string | null
direct_price?: number | null
title?: string | null
wallet?: { gold?: number | null } | null
}
/**
* موجودی محصول به واحد خود محصول:
* - تعدادی → تعداد قلم (عدد)
* - وزنی → وزن به واحد محصول (گرم/کیلوگرم)
* همسان با inventory_cards سرور (wallet.gold مستقیماً به واحد محصول است).
*/
export function resolveProductQuantity(
product: DeliveryProduct | null | undefined,
quantity?: number | null
): number {
if (!product) {
return 0
}
const qty = quantity != null ? Number(quantity) : Number(product.wallet?.gold ?? 0)
if (!Number.isFinite(qty) || qty <= 0) {
return 0
}
return qty
}
/** قالب‌بندی مقدار بر اساس واحد: عدد بدون اعشار، وزن با فرمت وزنی */
function formatDeliveryQuantity(value: number, product: DeliveryProduct | null | undefined): string {
if (isCountBasedProduct(product as any)) {
return usePriceWithoutType(value, false)
}
return formatWeightAmount(value)
}
/**
* وضعیت تحویل فیزیکی یک محصول:
* - none: محصول اصلاً تحویل فیزیکی ندارد
* - ready: موجودی به حداقل رسیده و تحویل فعال است
* - below_min: تحویل دارد ولی هنوز به حداقل نرسیده — پیام «چند واحد مانده»
*/
export function resolvePhysicalDeliveryState(
product: DeliveryProduct | null | undefined,
quantity?: number | null
): PhysicalDeliveryState {
const title = product?.title?.trim() || 'این محصول'
const unit = getProductQuantityUnit(product as any)
if (!product?.has_physical_delivery) {
return {
status: 'none',
available: 0,
min: 0,
remaining: 0,
unit,
message: `${title} تحویل فیزیکی ندارد`,
}
}
const min = Math.max(0, Number(product.min_physical_delivery_quantity ?? 0))
const available = resolveProductQuantity(product, quantity)
if (min <= 0 || available + 0.0001 >= min) {
return {
status: 'ready',
available,
min,
remaining: 0,
unit,
message: 'امکان تحویل فیزیکی فعال است',
}
}
const remaining = Math.max(0, min - available)
return {
status: 'below_min',
available,
min,
remaining,
unit,
message: `${formatDeliveryQuantity(remaining, product)} ${unit} مانده تا رسیدن به حداقل تحویل فیزیکی`,
}
}
+39
View File
@@ -0,0 +1,39 @@
import { persianToEnglishDigits } from '@/utils/persianDigits'
/** استخراج کد عددی از نتیجه WebOTP / Credential */
export function extractOtpCode(
credential: Credential | null | undefined,
length = 4
): string {
const raw = (credential as { code?: string } | null | undefined)?.code ?? ''
return persianToEnglishDigits(raw).replace(/\D/g, '').slice(0, length)
}
/**
* گوش دادن به SMS از طریق WebOTP API (Chrome Android).
* با کلیک Allow روی دیالوگ مرورگر، onCode با کد ۴ رقمی صدا زده می‌شود.
*/
export function listenForSmsOtp(onCode: (code: string) => void): AbortController {
const ac = new AbortController()
if (!('OTPCredential' in window) || !navigator.credentials?.get) {
return ac
}
void navigator.credentials
.get({
otp: { transport: ['sms'] },
signal: ac.signal,
} as CredentialRequestOptions)
.then((credential) => {
const code = extractOtpCode(credential, 4)
if (code.length === 4) {
onCode(code)
}
})
.catch(() => {
/* aborted or denied — ignore */
})
return ac
}