1405/04/27
-responive bugs
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
export function resolveMetalTypeLabel(settingValue?: string | number | null): string {
|
||||
const value = settingValue?.toString().trim()
|
||||
return value ? value : 'طلا'
|
||||
}
|
||||
|
||||
export function resolveMeltedGoldLabel(settingValue?: string | number | null): string {
|
||||
const value = settingValue?.toString().trim()
|
||||
return value ? value : 'آبشده'
|
||||
}
|
||||
@@ -39,7 +39,7 @@ export function getProductDisplayImage(product?: {
|
||||
return resolveProductImageSrc(product)
|
||||
}
|
||||
|
||||
import { formatGoldAmount } from '@/utils/usePrice'
|
||||
import { formatGoldAmount, usePriceWithoutType } from '@/utils/usePrice'
|
||||
|
||||
export function formatMetalTotal(
|
||||
metal: WalletMetalTotal,
|
||||
@@ -71,3 +71,44 @@ export function resolveAssetsRialValue(
|
||||
?.metal_totals?.[0]?.total_weight
|
||||
return cash + Number(metalWeight ?? 0) * liveGoldPrice
|
||||
}
|
||||
|
||||
export function getProductInventoryQuantity(
|
||||
cards: WalletInventoryCard[],
|
||||
productId?: number | null,
|
||||
mainWalletGold?: number | null,
|
||||
): number {
|
||||
const card = cards.find((item) => Number(item.product_id) === Number(productId))
|
||||
return card ? Number(card.quantity ?? 0) : Number(mainWalletGold ?? 0)
|
||||
}
|
||||
|
||||
export function formatProductWalletBalanceText(
|
||||
product:
|
||||
| { id?: number; title?: string | null; unit?: string | null; group_type?: string | null }
|
||||
| null,
|
||||
cards: WalletInventoryCard[] = [],
|
||||
mainWalletGold?: number | null,
|
||||
): string {
|
||||
const card = cards.find((item) => Number(item.product_id) === Number(product?.id))
|
||||
const quantity = card ? Number(card.quantity ?? 0) : Number(mainWalletGold ?? 0)
|
||||
const unit = card?.unit ?? product?.unit?.trim() ?? 'گرم'
|
||||
const title = product?.title?.trim() || 'محصول'
|
||||
const formatted = unit === 'عدد'
|
||||
? usePriceWithoutType(quantity, false)
|
||||
: formatGoldAmount(quantity)
|
||||
|
||||
return `موجودی کیف ${title}: ${formatted} ${unit}`
|
||||
}
|
||||
|
||||
export function getCashWalletBalance(
|
||||
walletInfo?: Record<string, unknown> | null,
|
||||
wallets?: unknown,
|
||||
): number {
|
||||
const direct = Number(walletInfo?.cash ?? walletInfo?.count_cash_wallets ?? 0)
|
||||
if (direct) return direct
|
||||
|
||||
if (Array.isArray(wallets)) {
|
||||
return wallets.reduce((sum, wallet) => sum + Number(wallet?.cash ?? 0), 0)
|
||||
}
|
||||
|
||||
return Number((wallets as { cash?: number } | null)?.cash ?? 0)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import { useSettingsStore } from '@/store/settingsStore'
|
||||
|
||||
export function getWeightUnit() {
|
||||
return useSettingsStore().tradeWeightUnit
|
||||
}
|
||||
|
||||
export function formatCashWalletBalanceText(
|
||||
cashFormatted: string | number,
|
||||
_groupType?: string | null,
|
||||
) {
|
||||
return `موجودی کیف پول: ${cashFormatted} ریال`
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
import type { Product } from '@/models/products'
|
||||
import { usePriceWithoutType } from '@/utils/usePrice'
|
||||
import { persianToEnglishDigits } from '@/utils/persianDigits'
|
||||
|
||||
type LimitProduct = Pick<Product, 'order_limit_enabled' | 'min_order_cash' | 'max_order_cash'> | null | undefined
|
||||
|
||||
function parseOrderLimitCash(value: unknown): number | undefined {
|
||||
if (value === undefined || value === null || value === '') {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const raw = persianToEnglishDigits(String(value).trim().replace(/[,\s٬]/g, ''))
|
||||
if (raw === '') {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const num = Number(raw)
|
||||
if (!Number.isFinite(num) || num < 0) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return Math.trunc(num)
|
||||
}
|
||||
|
||||
function isOrderLimitFlagEnabled(value: unknown): boolean {
|
||||
if (value === true || value === 1) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (typeof value === 'string') {
|
||||
const normalized = value.trim().toLowerCase()
|
||||
return normalized === '1' || normalized === 'true'
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
export function productOrderLimitEnabled(product?: LimitProduct): boolean {
|
||||
if (!product) {
|
||||
return false
|
||||
}
|
||||
|
||||
const min = parseOrderLimitCash(product.min_order_cash)
|
||||
const max = parseOrderLimitCash(product.max_order_cash)
|
||||
if (min != null || max != null) {
|
||||
return true
|
||||
}
|
||||
|
||||
return isOrderLimitFlagEnabled(product.order_limit_enabled)
|
||||
}
|
||||
|
||||
export function productMinOrderCash(product?: LimitProduct, isBuy = true, defaultBuyMin = 500_000): number {
|
||||
const min = parseOrderLimitCash(product?.min_order_cash)
|
||||
if (productOrderLimitEnabled(product) && min != null) {
|
||||
return min
|
||||
}
|
||||
|
||||
return isBuy ? defaultBuyMin : 0
|
||||
}
|
||||
|
||||
export function productMaxOrderCash(product?: LimitProduct): number | undefined {
|
||||
const max = parseOrderLimitCash(product?.max_order_cash)
|
||||
if (productOrderLimitEnabled(product) && max != null) {
|
||||
return max
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
export function productCashValidationMessage(
|
||||
product: LimitProduct,
|
||||
goldCash: number | string | undefined,
|
||||
isBuy: boolean,
|
||||
formatPrice: (value: number) => string = usePriceWithoutType
|
||||
): string {
|
||||
if (goldCash === '' || goldCash === undefined) {
|
||||
return ''
|
||||
}
|
||||
|
||||
const cash = parseOrderLimitCash(goldCash) ?? 0
|
||||
const min = productMinOrderCash(product, isBuy)
|
||||
const max = productMaxOrderCash(product)
|
||||
|
||||
if (max != null && cash > max) {
|
||||
return `حداکثر مبلغ این محصول ${formatPrice(max)} ریال است`
|
||||
}
|
||||
|
||||
if (min > 0 && cash < min) {
|
||||
if (productOrderLimitEnabled(product) && parseOrderLimitCash(product?.min_order_cash) != null) {
|
||||
return `حداقل مبلغ این محصول ${formatPrice(min)} ریال است`
|
||||
}
|
||||
|
||||
return `حداقل مبلغ طلا جهت ${isBuy ? 'خرید' : 'فروش'} ${formatPrice(min)} ریال می باشد.`
|
||||
}
|
||||
|
||||
return ''
|
||||
}
|
||||
|
||||
export function validateProductOrderLimitForm(form: {
|
||||
order_limit_enabled?: boolean
|
||||
min_order_cash?: number | string | null
|
||||
max_order_cash?: number | string | null
|
||||
}): string | null {
|
||||
if (!form.order_limit_enabled) return null
|
||||
|
||||
const min = form.min_order_cash
|
||||
const max = form.max_order_cash
|
||||
const hasMin = min != null && min !== ''
|
||||
const hasMax = max != null && max !== ''
|
||||
|
||||
if (!hasMin && !hasMax) {
|
||||
return 'با فعال بودن محدودیت مبلغ سفارش، حداقل یکی از حداقل یا حداکثر مبلغ باید وارد شود'
|
||||
}
|
||||
|
||||
if (hasMin && hasMax && Number(min) > Number(max)) {
|
||||
return 'حداقل مبلغ نمیتواند بیشتر از حداکثر باشد'
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import type { Product } from '@/models/products'
|
||||
|
||||
type TradeOpenProduct = Pick<Product, 'open_buy' | 'open_sell'> | null | undefined
|
||||
|
||||
function asBool(value: boolean | number | null | undefined) {
|
||||
return value === true || Number(value) === 1
|
||||
}
|
||||
|
||||
export function isProductBuyOpen(product: TradeOpenProduct) {
|
||||
if (!product) return true
|
||||
return asBool(product?.open_buy)
|
||||
}
|
||||
|
||||
export function isProductSellOpen(product: TradeOpenProduct) {
|
||||
if (!product) return true
|
||||
return asBool(product?.open_sell)
|
||||
}
|
||||
|
||||
export function isAnyProductTradeOpen(product: TradeOpenProduct) {
|
||||
return isProductBuyOpen(product) || isProductSellOpen(product)
|
||||
}
|
||||
|
||||
export function isProductTradeOpen(product: TradeOpenProduct, isBuy: boolean) {
|
||||
return isBuy ? isProductBuyOpen(product) : isProductSellOpen(product)
|
||||
}
|
||||
|
||||
/** تب اولیه با توجه به query و وضعیت باز/بسته خرید و فروش */
|
||||
export function resolveProductTradeIsBuy(product: TradeOpenProduct, routeState?: string | null) {
|
||||
const wantSell = routeState === 'sell'
|
||||
const wantBuy = routeState === 'buy'
|
||||
const buyOpen = isProductBuyOpen(product)
|
||||
const sellOpen = isProductSellOpen(product)
|
||||
|
||||
if (wantSell && sellOpen) return false
|
||||
if (wantBuy && buyOpen) return true
|
||||
if (buyOpen) return true
|
||||
if (sellOpen) return false
|
||||
return true
|
||||
}
|
||||
|
||||
export function productTradeClosedMessage(isBuy: boolean) {
|
||||
return isBuy
|
||||
? 'خرید این محصول در این لحظه مقدور نمیباشد'
|
||||
: 'فروش این محصول در این لحظه مقدور نمیباشد'
|
||||
}
|
||||
+310
-68
@@ -3,28 +3,63 @@ import { getSettings } from '@/api/configuration'
|
||||
|
||||
export const serviceFeePercentage = ref<number>(0.01)
|
||||
|
||||
/** true = مبالغ رند: فروش → ریال دقیق (کارمزد از طلا)؛ خرید → وزن دقیق (کارمزد از ریال) */
|
||||
/**
|
||||
* @deprecated منطق کارمزد دیگر به این کلید وابسته نیست.
|
||||
* قانون واحد: کاربر هر فیلدی را که پر کند دقیقاً همان معامله میشود و کارمزد از سمت دیگر کسر میشود:
|
||||
* • ریال قفل (کاربر مبلغ زده) → مبلغ دقیق، کارمزد از طلا.
|
||||
* • گرم قفل (کاربر وزن زده) → وزن دقیق، کارمزد از ریال.
|
||||
*/
|
||||
export const feeDeductFromGold = ref(false)
|
||||
|
||||
/** آخرین فیلدی که کاربر مستقیم ویرایش کرده — مبنای بهروزرسانی هنگام تغییر قیمت */
|
||||
export type TradeInputLock = 'gram' | 'rial' | null
|
||||
|
||||
/** ریال قفل است؟ (کاربر مبلغ ریالی را مستقیم وارد کرده) → کارمزد از طلا */
|
||||
export function isRialLocked(inputLock?: TradeInputLock): boolean {
|
||||
return inputLock === 'rial'
|
||||
}
|
||||
|
||||
/** حداکثر اعشار ذخیره/ارسال به سرور (مطابق orders.amount) */
|
||||
export const TRADE_GRAM_STORAGE_DECIMALS = 10
|
||||
|
||||
/** حداکثر اعشار نمایش به کاربر در اپ */
|
||||
export const TRADE_GRAM_DISPLAY_DECIMALS = 8
|
||||
|
||||
/** زیر این مقدار (گرم)، معادل گرم در فیلد ورودی نشان داده نمیشود */
|
||||
export const TRADE_GRAM_MIN_DISPLAY_GRAMS = 0.0001
|
||||
|
||||
function trimTrailingDecimalZeros(fixed: string): string {
|
||||
if (!fixed.includes('.')) {
|
||||
return fixed
|
||||
}
|
||||
|
||||
const trimmed = fixed.replace(/\.?0+$/, '')
|
||||
return trimmed === '' ? '0' : trimmed
|
||||
}
|
||||
|
||||
function formatTradeGramFixedText(grams: number): string {
|
||||
return trimTrailingDecimalZeros(grams.toFixed(TRADE_GRAM_DISPLAY_DECIMALS))
|
||||
}
|
||||
|
||||
export function normalizeOrderGoldAmount(grams?: number | null): number {
|
||||
const value = Number(grams ?? 0)
|
||||
if (!Number.isFinite(value) || value === 0) return 0
|
||||
return parseFloat(value.toFixed(TRADE_GRAM_STORAGE_DECIMALS))
|
||||
}
|
||||
|
||||
export function formatTradeGramDisplay(grams?: number | null): number {
|
||||
/** نمایش گرم در UI — همیشه اعشاری، بدون نماد e */
|
||||
export function formatTradeGramDisplay(grams?: number | null): string {
|
||||
const value = Number(grams ?? 0)
|
||||
if (!Number.isFinite(value)) return 0
|
||||
return parseFloat(value.toFixed(TRADE_GRAM_DISPLAY_DECIMALS))
|
||||
if (!Number.isFinite(value) || value === 0) return '0'
|
||||
return formatTradeGramFixedText(value)
|
||||
}
|
||||
|
||||
/** مقدار فیلد گرم هنگام تبدیل از ریال — مقادیر خیلی ریز خالی میمانند */
|
||||
export function formatTradeGramInputValue(grams?: number | null): string | undefined {
|
||||
const value = Number(grams ?? 0)
|
||||
if (!Number.isFinite(value) || value <= 0) return undefined
|
||||
if (value < TRADE_GRAM_MIN_DISPLAY_GRAMS) return undefined
|
||||
return formatTradeGramFixedText(value)
|
||||
}
|
||||
|
||||
function feeRate() {
|
||||
@@ -67,44 +102,36 @@ export function calcGoldAmountFromTargetRial(rialAmount: number, unitPriceRial:
|
||||
return normalizeOrderGoldAmount(rialAmount / unitPriceRial)
|
||||
}
|
||||
|
||||
/** وقتی کاربر گرم را قفل کرده — ریال نمایشی/قابل پرداخت از همان گرم */
|
||||
/**
|
||||
* کاربر گرم را قفل کرده → وزن دقیق است و کارمزد از ریال کسر میشود.
|
||||
* خرید: مبلغ پرداختی = وزن × مظنه × (۱ + کارمزد).
|
||||
* فروش: مبلغ دریافتی = وزن × مظنه × (۱ − کارمزد).
|
||||
*/
|
||||
export function calcRialFromLockedGram(displayGram: number, unitPriceRial: number, isBuy: boolean) {
|
||||
const grams = Number(displayGram)
|
||||
const unit = Number(unitPriceRial)
|
||||
if (!grams || !unit) return 0
|
||||
const fee = feeRate()
|
||||
|
||||
if (isBuy) {
|
||||
if (feeDeductFromGold.value) {
|
||||
return Math.round(grams * unit * (1 + fee))
|
||||
}
|
||||
return Math.round(grams * unit)
|
||||
}
|
||||
|
||||
if (feeDeductFromGold.value) {
|
||||
return Math.round(grams * unit)
|
||||
}
|
||||
return Math.round(grams * unit * (1 - fee))
|
||||
return isBuy
|
||||
? Math.round(grams * unit * (1 + fee))
|
||||
: Math.round(grams * unit * (1 - fee))
|
||||
}
|
||||
|
||||
/** وقتی کاربر ریال را قفل کرده — گرم نمایشی از همان مبلغ */
|
||||
/**
|
||||
* کاربر ریال را قفل کرده → مبلغ دقیق است و کارمزد از طلا کسر میشود.
|
||||
* خرید: طلای دریافتی = (مبلغ ÷ مظنه) × (۱ − کارمزد) (خالص واریزی به کیف).
|
||||
* فروش: طلای کسرشونده از کیف = (مبلغ ÷ مظنه) × (۱ + کارمزد) (شامل کارمزد).
|
||||
*/
|
||||
export function calcGramFromLockedRial(rialAmount: number, unitPriceRial: number, isBuy: boolean) {
|
||||
const rial = Number(rialAmount)
|
||||
const unit = Number(unitPriceRial)
|
||||
if (!rial || !unit) return 0
|
||||
const fee = feeRate()
|
||||
|
||||
if (isBuy) {
|
||||
if (feeDeductFromGold.value) {
|
||||
return rial / (unit * (1 + fee))
|
||||
}
|
||||
return (rial / unit) * (1 - fee)
|
||||
}
|
||||
|
||||
if (feeDeductFromGold.value) {
|
||||
return rial / unit
|
||||
}
|
||||
return rial / (unit * (1 - fee))
|
||||
return isBuy
|
||||
? (rial / unit) * (1 - fee)
|
||||
: (rial / unit) * (1 + fee)
|
||||
}
|
||||
|
||||
/** مبلغ ریالی قفلشده توسط کاربر — برای ارسال به سرور */
|
||||
@@ -118,7 +145,11 @@ export function calcTradeTargetCash(
|
||||
return Math.round(cash)
|
||||
}
|
||||
|
||||
/** مبلغ نهایی قابل پرداخت (خرید) یا دریافت (فروش) به ریال */
|
||||
/**
|
||||
* مبلغ نهایی قابل پرداخت (خرید) یا دریافت (فروش) به ریال.
|
||||
* • ریال قفل (targetRialCash موجود) → دقیقاً همان مبلغ (کارمزد از طلاست، روی ریال اثر ندارد).
|
||||
* • گرم قفل → کارمزد از ریال: خرید = ارزش + کارمزد، فروش = ارزش − کارمزد.
|
||||
*/
|
||||
export function calcPayablePriceRial(
|
||||
finalPriceRial: number,
|
||||
feeInRial: number,
|
||||
@@ -126,17 +157,21 @@ export function calcPayablePriceRial(
|
||||
targetRialCash?: number | null
|
||||
) {
|
||||
if (targetRialCash != null && Number(targetRialCash) > 0) {
|
||||
return Number(targetRialCash)
|
||||
return Math.round(Number(targetRialCash))
|
||||
}
|
||||
|
||||
if (feeDeductFromGold.value) {
|
||||
if (isBuy) return finalPriceRial + feeInRial
|
||||
return finalPriceRial
|
||||
}
|
||||
return isBuy ? finalPriceRial : Math.max(0, finalPriceRial - feeInRial)
|
||||
return isBuy
|
||||
? Math.round(finalPriceRial + feeInRial)
|
||||
: Math.max(0, Math.round(finalPriceRial - feeInRial))
|
||||
}
|
||||
|
||||
/** وزن ارسالی به سرور */
|
||||
/**
|
||||
* وزن مبنای معامله که به سرور ارسال میشود.
|
||||
* • گرم قفل → همان وزن واردشده (دقیق).
|
||||
* • ریال قفل خرید → طلای خالص دریافتی = (مبلغ ÷ مظنه) × (۱ − کارمزد).
|
||||
* • ریال قفل فروش → وزن معادلِ مبلغ = مبلغ ÷ مظنه (کارمزد جدا از این وزن کسر میشود).
|
||||
* در حالت ریالقفل، سرور مقدار را از target_cash بازمحاسبه میکند؛ این مقدار صرفاً نمایشی/پشتیبان است.
|
||||
*/
|
||||
export function calcOrderGoldAmount(
|
||||
goldValue?: number | null,
|
||||
goldCash?: number | null,
|
||||
@@ -147,60 +182,267 @@ export function calcOrderGoldAmount(
|
||||
const gold = Number(goldValue ?? 0)
|
||||
const cash = Number(goldCash ?? 0)
|
||||
const unit = Number(unitPriceRial ?? 0)
|
||||
const fee = feeRate()
|
||||
|
||||
if (isBuy && inputLock === 'rial' && cash > 0 && unit > 0) {
|
||||
if (feeDeductFromGold.value) {
|
||||
const fee = feeRate()
|
||||
return normalizeOrderGoldAmount(cash / (unit * (1 + fee)))
|
||||
if (inputLock === 'rial' && cash > 0 && unit > 0) {
|
||||
if (isBuy) {
|
||||
return normalizeOrderGoldAmount((cash / unit) * (1 - fee))
|
||||
}
|
||||
return normalizeOrderGoldAmount(cash / unit)
|
||||
}
|
||||
|
||||
if (!isBuy && feeDeductFromGold.value) {
|
||||
if (inputLock === 'rial' && cash > 0 && unit > 0) {
|
||||
return calcGoldAmountFromTargetRial(cash, unit)
|
||||
}
|
||||
if (inputLock !== 'rial') {
|
||||
return normalizeOrderGoldAmount(gold)
|
||||
}
|
||||
const targetCash = calcSellTargetCash(goldCash, goldValue, unitPriceRial, inputLock)
|
||||
if (targetCash != null && unit > 0) {
|
||||
return calcGoldAmountFromTargetRial(targetCash, unit)
|
||||
}
|
||||
}
|
||||
|
||||
return normalizeOrderGoldAmount(gold)
|
||||
}
|
||||
|
||||
/** مبلغ ریالی دقیق دریافتی در فروش (فقط وقتی کاربر با ریال وارد کرده) */
|
||||
/** مبلغ ریالی دقیق (فروش یا خرید) وقتی کاربر ریال را قفل کرده — همان چیزی که به سرور بهعنوان target_cash میرود */
|
||||
export function calcSellTargetCash(
|
||||
goldCash?: number | null,
|
||||
goldValue?: number | null,
|
||||
unitPriceRial?: number,
|
||||
inputLock?: TradeInputLock
|
||||
) {
|
||||
if (!feeDeductFromGold.value) return undefined
|
||||
if (inputLock === 'gram') return undefined
|
||||
|
||||
if (inputLock !== 'rial') return undefined
|
||||
const cash = Number(goldCash ?? 0)
|
||||
if (cash <= 0) return undefined
|
||||
if (inputLock === 'rial') return Math.round(cash)
|
||||
|
||||
const gold = Number(goldValue ?? 0)
|
||||
const unit = Number(unitPriceRial ?? 0)
|
||||
if (gold > 0 && unit > 0) {
|
||||
const derived = Math.round(gold * unit)
|
||||
if (Math.abs(derived - Math.round(cash)) <= 1) return undefined
|
||||
}
|
||||
return Math.round(cash)
|
||||
}
|
||||
|
||||
/** کل طلای کسرشونده از کیف در فروش (شامل کارمزد وقتی fee_deduct_from_gold روشن است) */
|
||||
export function calcSellGoldDebit(goldValue?: number | null, feeInGrams?: number | null) {
|
||||
/**
|
||||
* کل طلای کسرشونده از کیف در فروش.
|
||||
* • ریال قفل → وزن معادل + کارمزد (کارمزد از طلا).
|
||||
* • گرم قفل → فقط همان وزن (کارمزد از ریال کسر میشود، نه از طلا).
|
||||
*/
|
||||
export function calcSellGoldDebit(
|
||||
goldValue?: number | null,
|
||||
feeInGrams?: number | null,
|
||||
inputLock?: TradeInputLock
|
||||
) {
|
||||
const gold = Number(goldValue ?? 0)
|
||||
const fee = Number(feeInGrams ?? 0)
|
||||
if (feeDeductFromGold.value) {
|
||||
if (isRialLocked(inputLock)) {
|
||||
return parseFloat((gold + fee).toFixed(4))
|
||||
}
|
||||
return gold
|
||||
}
|
||||
|
||||
export type FeeDeductionAsset = 'gold' | 'rial'
|
||||
|
||||
/** کارمزد از کدام دارایی کسر میشود — فقط تابع قفل ورودی است (نه خرید/فروش، نه کلید تنظیمات) */
|
||||
export function feeDeductedFrom(inputLock?: TradeInputLock): FeeDeductionAsset {
|
||||
return isRialLocked(inputLock) ? 'gold' : 'rial'
|
||||
}
|
||||
|
||||
/** طلای خالص واریزی به کیف پس از کسر کارمزد (فقط خرید) */
|
||||
export function calcNetGoldCredited(
|
||||
orderGoldAmount: number,
|
||||
feeInGrams: number,
|
||||
isBuy: boolean,
|
||||
inputLock?: TradeInputLock
|
||||
): number {
|
||||
if (!isBuy) return orderGoldAmount
|
||||
// ریال قفل: orderGoldAmount از قبل خالص است. گرم قفل: کارمزد از ریال است، پس همان وزن دریافت میشود.
|
||||
return normalizeOrderGoldAmount(orderGoldAmount)
|
||||
}
|
||||
|
||||
/** مبلغ پایه معامله قبل از اعمال کارمزد ریالی (خرید با افزودن کارمزد به ریال) */
|
||||
export function calcTradeBasePriceRial(orderGoldAmount: number, unitPriceRial: number) {
|
||||
return Math.round(Number(orderGoldAmount) * Number(unitPriceRial))
|
||||
}
|
||||
|
||||
export interface TradeConfirmRow {
|
||||
label: string
|
||||
gram?: number | null
|
||||
rial?: number | null
|
||||
bold?: boolean
|
||||
minus?: boolean
|
||||
plus?: boolean
|
||||
dividerBefore?: boolean
|
||||
}
|
||||
|
||||
export interface TradeConfirmSection {
|
||||
heading: string
|
||||
rows: TradeConfirmRow[]
|
||||
footnote?: string
|
||||
}
|
||||
|
||||
export interface TradeConfirmBreakdown {
|
||||
sections: TradeConfirmSection[]
|
||||
formHint: string
|
||||
}
|
||||
|
||||
function hasTradeFee(feeInGrams: number, feeInRial: number) {
|
||||
return feeInGrams > 0 || feeInRial > 0
|
||||
}
|
||||
|
||||
function grossGoldFromRial(rial: number, unitPriceRial: number) {
|
||||
if (!rial || !unitPriceRial) return 0
|
||||
return normalizeOrderGoldAmount(rial / unitPriceRial)
|
||||
}
|
||||
|
||||
/**
|
||||
* جزئیات مدال تأیید — دو بخش «کسر از کیف» و «واریز به کیف».
|
||||
* فقط بر پایهٔ قفل ورودی تصمیم میگیرد:
|
||||
* • ریال قفل → مبلغ دقیق، کارمزد از طلا.
|
||||
* • گرم قفل → وزن دقیق، کارمزد از ریال.
|
||||
*/
|
||||
export function buildTradeConfirmBreakdown(params: {
|
||||
isBuy: boolean
|
||||
inputLock: TradeInputLock
|
||||
orderGoldAmount: number
|
||||
feeInGrams: number
|
||||
feeInRial: number
|
||||
payablePrice: number
|
||||
unitPriceRial: number
|
||||
sellGoldDebit?: number
|
||||
}): TradeConfirmBreakdown {
|
||||
const {
|
||||
isBuy,
|
||||
inputLock,
|
||||
orderGoldAmount,
|
||||
feeInGrams,
|
||||
feeInRial,
|
||||
payablePrice,
|
||||
unitPriceRial,
|
||||
sellGoldDebit,
|
||||
} = params
|
||||
const hasFee = hasTradeFee(feeInGrams, feeInRial)
|
||||
const rialLocked = isRialLocked(inputLock)
|
||||
const weight = orderGoldAmount
|
||||
|
||||
if (!isBuy) {
|
||||
// ————— فروش —————
|
||||
if (rialLocked) {
|
||||
// مبلغ دقیق دریافتی، کارمزد از طلا
|
||||
const debit = sellGoldDebit ?? parseFloat((weight + feeInGrams).toFixed(4))
|
||||
const goldRows: TradeConfirmRow[] = hasFee
|
||||
? [
|
||||
{ label: 'وزن معادل مبلغ', gram: weight },
|
||||
{ label: 'کارمزد', gram: feeInGrams, minus: true },
|
||||
{ label: 'جمع کسر از کیف طلا', gram: debit, bold: true, dividerBefore: true },
|
||||
]
|
||||
: [{ label: 'وزن فروش', gram: weight, bold: true }]
|
||||
|
||||
return {
|
||||
sections: [
|
||||
{ heading: 'از کیف طلا کسر میشود', rows: goldRows },
|
||||
{
|
||||
heading: 'به کیف پول واریز میشود',
|
||||
rows: [{ label: 'مبلغ دریافتی', rial: payablePrice, bold: true }],
|
||||
footnote: hasFee
|
||||
? 'دقیقاً همان مبلغی که وارد کردید واریز میشود؛ کارمزد فقط از طلای کیف کسر میشود.'
|
||||
: undefined,
|
||||
},
|
||||
],
|
||||
formHint: hasFee
|
||||
? `${payablePrice.toLocaleString('fa-IR')} ریال دریافت — ${formatTradeGramDisplay(debit)} گرم از کیف کسر میشود (شامل ${formatTradeGramDisplay(feeInGrams)} گرم کارمزد)`
|
||||
: `${payablePrice.toLocaleString('fa-IR')} ریال دریافت`,
|
||||
}
|
||||
}
|
||||
|
||||
// گرم قفل: وزن دقیق، کارمزد از ریال
|
||||
const grossSellRial = Math.round(weight * unitPriceRial)
|
||||
const cashRows: TradeConfirmRow[] = hasFee
|
||||
? [
|
||||
{ label: 'ارزش فروش', rial: grossSellRial },
|
||||
{ label: 'کارمزد', rial: feeInRial, minus: true },
|
||||
{ label: 'خالص دریافتی', rial: payablePrice, bold: true, dividerBefore: true },
|
||||
]
|
||||
: [{ label: 'مبلغ دریافتی', rial: payablePrice, bold: true }]
|
||||
|
||||
return {
|
||||
sections: [
|
||||
{ heading: 'از کیف طلا کسر میشود', rows: [{ label: 'وزن فروش', gram: weight, bold: true }] },
|
||||
{
|
||||
heading: 'به کیف پول واریز میشود',
|
||||
rows: cashRows,
|
||||
footnote: hasFee ? 'کارمزد از مبلغ ریالی دریافتی کم شده است.' : undefined,
|
||||
},
|
||||
],
|
||||
formHint: hasFee
|
||||
? `${formatTradeGramDisplay(weight)} گرم فروش — ${feeInRial.toLocaleString('fa-IR')} ریال کارمزد از مبلغ دریافتی کسر میشود`
|
||||
: `${formatTradeGramDisplay(weight)} گرم فروش — ${payablePrice.toLocaleString('fa-IR')} ریال به کیف پول`,
|
||||
}
|
||||
}
|
||||
|
||||
// ————— خرید —————
|
||||
if (rialLocked) {
|
||||
// مبلغ دقیق پرداختی، کارمزد از طلا
|
||||
const grossGram = grossGoldFromRial(payablePrice, unitPriceRial)
|
||||
const feeGram = parseFloat(Math.max(0, grossGram - weight).toFixed(4))
|
||||
const goldRows: TradeConfirmRow[] = hasFee
|
||||
? [
|
||||
{ label: 'معادل طلای خام', gram: grossGram },
|
||||
{ label: 'کارمزد', gram: feeGram, minus: true },
|
||||
{ label: 'خالص واریزی به کیف طلا', gram: weight, bold: true, dividerBefore: true },
|
||||
]
|
||||
: [{ label: 'واریزی به کیف طلا', gram: weight, bold: true }]
|
||||
|
||||
return {
|
||||
sections: [
|
||||
{
|
||||
heading: 'از کیف پول کسر میشود',
|
||||
rows: [{ label: 'مبلغ پرداختی', rial: payablePrice, bold: true }],
|
||||
footnote: 'دقیقاً همان مبلغی که وارد کردید؛ کارمزد جداگانه اضافه نمیشود.',
|
||||
},
|
||||
{
|
||||
heading: 'به کیف طلا واریز میشود',
|
||||
rows: goldRows,
|
||||
footnote: hasFee ? 'کارمزد از وزن طلای دریافتی کسر شده است.' : undefined,
|
||||
},
|
||||
],
|
||||
formHint: hasFee
|
||||
? `${payablePrice.toLocaleString('fa-IR')} ریال پرداخت — ${formatTradeGramDisplay(weight)} گرم واریز (کارمزد از طلا کسر شد)`
|
||||
: `${payablePrice.toLocaleString('fa-IR')} ریال پرداخت`,
|
||||
}
|
||||
}
|
||||
|
||||
// گرم قفل خرید: وزن دقیق، کارمزد از ریال
|
||||
const basePriceRial = calcTradeBasePriceRial(weight, unitPriceRial)
|
||||
const cashRows: TradeConfirmRow[] = hasFee
|
||||
? [
|
||||
{ label: `ارزش ${formatTradeGramDisplay(weight)} گرم`, rial: basePriceRial },
|
||||
{ label: 'کارمزد', rial: feeInRial, plus: true },
|
||||
{ label: 'جمع پرداختی', rial: payablePrice, bold: true, dividerBefore: true },
|
||||
]
|
||||
: [{ label: 'مبلغ پرداختی', rial: payablePrice, bold: true }]
|
||||
|
||||
return {
|
||||
sections: [
|
||||
{
|
||||
heading: 'از کیف پول کسر میشود',
|
||||
rows: cashRows,
|
||||
footnote: hasFee ? 'کارمزد به مبلغ پرداختی اضافه شده است.' : undefined,
|
||||
},
|
||||
{
|
||||
heading: 'به کیف طلا واریز میشود',
|
||||
rows: [{ label: 'دقیقاً', gram: weight, bold: true }],
|
||||
},
|
||||
],
|
||||
formHint: hasFee
|
||||
? `${payablePrice.toLocaleString('fa-IR')} ریال (شامل ${feeInRial.toLocaleString('fa-IR')} ریال کارمزد) — ${formatTradeGramDisplay(weight)} گرم واریز`
|
||||
: `${payablePrice.toLocaleString('fa-IR')} ریال پرداخت`,
|
||||
}
|
||||
}
|
||||
|
||||
/** @deprecated از buildTradeConfirmBreakdown استفاده کنید */
|
||||
export function buildTradeFeeConfirmCopy(params: {
|
||||
isBuy: boolean
|
||||
inputLock: TradeInputLock
|
||||
orderGoldAmount: number
|
||||
feeInGrams: number
|
||||
feeInRial: number
|
||||
payablePrice: number
|
||||
unitPriceRial: number
|
||||
sellGoldDebit?: number
|
||||
}) {
|
||||
const breakdown = buildTradeConfirmBreakdown(params)
|
||||
return {
|
||||
feeDeductFrom: feeDeductedFrom(params.inputLock),
|
||||
feeDeductionLabel: '',
|
||||
feeDeductionHint: breakdown.formHint,
|
||||
payableNote: breakdown.sections.at(-1)?.footnote ?? '',
|
||||
showNetGoldRow: false,
|
||||
showSellDebitRow: false,
|
||||
showBasePriceRow: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
import { getSettings } from '@/api/configuration'
|
||||
import type { AppSetting } from '@/store/settingsStore'
|
||||
import { useSettingsStore } from '@/store/settingsStore'
|
||||
|
||||
export const TRACKING_SYSTEMS_KEY = 'tracking_systems'
|
||||
export const TRACKING_SYSTEMS_ENABLED_KEY = 'tracking_systems_enabled'
|
||||
|
||||
export interface TrackingSystemSite {
|
||||
id: string
|
||||
name: string
|
||||
url: string
|
||||
}
|
||||
|
||||
let trackingSettingsCache: AppSetting[] | null = null
|
||||
let trackingSettingsPromise: Promise<AppSetting[]> | null = null
|
||||
|
||||
export function normalizeSettingsResponse(data: unknown): AppSetting[] {
|
||||
if (Array.isArray(data)) return data as AppSetting[]
|
||||
|
||||
if (data && typeof data === 'object') {
|
||||
const record = data as Record<string, unknown>
|
||||
const nested = record.data ?? record.settings ?? record.rows
|
||||
if (Array.isArray(nested)) return nested as AppSetting[]
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
export function findSettingValue(settings: AppSetting[], key: string): unknown {
|
||||
const setting = settings.find((item) => String(item?.key ?? '').trim() === key)
|
||||
return setting?.value ?? null
|
||||
}
|
||||
|
||||
export function isSettingTruthy(value: unknown): boolean {
|
||||
if (value === true) return true
|
||||
const normalized = String(value ?? '').trim().toLowerCase()
|
||||
return normalized === 'true' || normalized === '1' || normalized === 'yes'
|
||||
}
|
||||
|
||||
export function parseTrackingSystems(raw: unknown): TrackingSystemSite[] {
|
||||
let value: unknown = raw
|
||||
|
||||
for (let depth = 0; depth < 2; depth += 1) {
|
||||
if (typeof value !== 'string') break
|
||||
const trimmed = value.trim()
|
||||
if (!trimmed) return []
|
||||
|
||||
try {
|
||||
value = JSON.parse(trimmed)
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
if (!Array.isArray(value)) return []
|
||||
|
||||
return value
|
||||
.map((item) => {
|
||||
if (!item || typeof item !== 'object') return null
|
||||
|
||||
const record = item as Record<string, unknown>
|
||||
const id = String(record.id ?? '').trim()
|
||||
const name = String(record.name ?? record.title ?? '').trim()
|
||||
const url = String(record.url ?? record.link ?? '').trim()
|
||||
|
||||
if (!name || !url) return null
|
||||
return { id: id || `${name}-${url}`, name, url }
|
||||
})
|
||||
.filter((item): item is TrackingSystemSite => item != null)
|
||||
}
|
||||
|
||||
export function resolveTrackingSystems(settings: AppSetting[]): TrackingSystemSite[] {
|
||||
return parseTrackingSystems(findSettingValue(settings, TRACKING_SYSTEMS_KEY))
|
||||
}
|
||||
|
||||
export function isTrackingSystemsEnabled(settings: AppSetting[]): boolean {
|
||||
return isSettingTruthy(findSettingValue(settings, TRACKING_SYSTEMS_ENABLED_KEY))
|
||||
}
|
||||
|
||||
export async function fetchTrackingSettings(force = false): Promise<AppSetting[]> {
|
||||
if (!force && trackingSettingsCache) return trackingSettingsCache
|
||||
if (!force && trackingSettingsPromise) return trackingSettingsPromise
|
||||
|
||||
trackingSettingsPromise = (async () => {
|
||||
const settingsStore = useSettingsStore()
|
||||
|
||||
try {
|
||||
await settingsStore.fetchSettings(force)
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
|
||||
try {
|
||||
const freshSettings = normalizeSettingsResponse(await getSettings())
|
||||
if (freshSettings.length > 0) settingsStore.mergeSettings(freshSettings)
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
|
||||
trackingSettingsCache = [...settingsStore.settings]
|
||||
return trackingSettingsCache
|
||||
})()
|
||||
.catch((error) => {
|
||||
console.log(error)
|
||||
trackingSettingsCache = []
|
||||
return []
|
||||
})
|
||||
.finally(() => {
|
||||
trackingSettingsPromise = null
|
||||
})
|
||||
|
||||
return trackingSettingsPromise
|
||||
}
|
||||
|
||||
export function resetTrackingSettingsCache() {
|
||||
trackingSettingsCache = null
|
||||
trackingSettingsPromise = null
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
export const TRADE_WEIGHT_UNIT_GRAM = 'گرم'
|
||||
export const TRADE_WEIGHT_UNIT_KILOGRAM = 'کیلوگرم'
|
||||
|
||||
export function isKilogramUnit(unit?: string | null) {
|
||||
const value = (unit ?? '').trim()
|
||||
return value === TRADE_WEIGHT_UNIT_KILOGRAM || value === 'kilogram' || value === 'kg'
|
||||
}
|
||||
|
||||
/** واحد نمایش از ستینگ — خالی یا نامعتبر = گرم */
|
||||
export function resolveTradeWeightUnit(settingValue?: string | number | null) {
|
||||
return isKilogramUnit(settingValue?.toString()) ? TRADE_WEIGHT_UNIT_KILOGRAM : TRADE_WEIGHT_UNIT_GRAM
|
||||
}
|
||||
|
||||
export function weightToGrams(value: number, fromUnit?: string | null) {
|
||||
return isKilogramUnit(fromUnit) ? value * 1000 : value
|
||||
}
|
||||
|
||||
export function gramsToTradeUnit(grams: number, displayUnit?: string | null) {
|
||||
return isKilogramUnit(displayUnit ?? resolveTradeWeightUnit(null)) ? grams / 1000 : grams
|
||||
}
|
||||
|
||||
export function convertWeightToDisplayUnit(
|
||||
value: number,
|
||||
fromUnit?: string | null,
|
||||
displayUnit?: string | null
|
||||
) {
|
||||
return gramsToTradeUnit(weightToGrams(value, fromUnit), displayUnit)
|
||||
}
|
||||
|
||||
export function formatTradeWeight(value: number, displayUnit?: string | null) {
|
||||
const unit = resolveTradeWeightUnit(displayUnit)
|
||||
return `${value} ${unit}`
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
/** Overlay dialogs (loading / blocking) */
|
||||
export const APP_Z_OVERLAY = 5600
|
||||
Reference in New Issue
Block a user