fix some bugs 4

This commit is contained in:
2026-07-18 17:49:50 +03:30
parent 6221308ecc
commit d5e452d02b
5 changed files with 642 additions and 108 deletions
+44
View File
@@ -1,5 +1,36 @@
import type { Customer } from '@/model/customer'
export interface OrderFeeBreakdownLine {
kind?: string
key?: string
label?: string
is_percent?: boolean
raw?: number
percent_of?: number | null
per_unit?: number
amount_units?: number
total?: number
gold?: number
rial?: number
detail?: string
line_text?: string
}
export interface OrderFeeBreakdown {
uses_app_price?: boolean
amount_units?: number
price_original?: number
price_customer?: number
base_total?: number
customer_total?: number
spread_per_unit?: number
spread_total?: number
service_charge_gold?: number
service_charge_rial?: number
grand_total?: number
lines?: OrderFeeBreakdownLine[]
}
export interface Order {
amount?: number
amount_display?: number
@@ -7,6 +38,19 @@ export interface Order {
order_metal_id?: number
order_metal_label?: string
metal_prices?: Record<string, number>
product_id?: number | null
product_title?: string | null
fee_source?: 'settings' | 'product' | 'app+product' | 'none' | string
fee_source_key?: string | null
fee_source_label?: string | null
fee_breakdown?: OrderFeeBreakdown | null
fee_display?: number
total_fee_display?: number
transactions?: Array<{
title?: string | null
product_id?: number | null
product?: { id?: number; title?: string | null } | null
}>
created_at?: string
customer?: Customer
customer_id?: number
@@ -0,0 +1,440 @@
<template>
<a-modal
v-model:visible="visibleProxy"
:title="null"
:footer="null"
centered
:width="modalWidth"
destroy-on-close
class="order-detail-modal"
@cancel="close"
>
<div v-if="order" class="order-detail">
<header class="order-detail__header">
<div class="min-w-0 flex-1">
<div class="flex flex-wrap items-center gap-2 mb-1">
<p class="order-detail__eyebrow m-0">جزئیات سفارش</p>
<span class="order-detail__badge" :class="directionClass">{{ directionLabel }}</span>
<span v-if="order.sts_str" class="order-detail__badge order-detail__badge--muted">
{{ order.sts_str }}
</span>
</div>
<h2 class="order-detail__title">{{ order.customer?.name || `سفارش #${order.id ?? '-'}` }}</h2>
<p class="order-detail__sub">
<span>{{ order.customer?.mobile || '-' }}</span>
<span v-if="order.id" class="order-detail__dot">·</span>
<span v-if="order.id">کد {{ order.id }}</span>
<span class="order-detail__dot">·</span>
<span class="order-detail__muted">{{ dateLabel }} {{ timeLabel }}</span>
</p>
<p class="order-detail__product-line m-0">
<span class="order-detail__field-label">محصول</span>
<strong>{{ productTitle || '—' }}</strong>
<span class="order-detail__chip">{{ order.order_metal_label || '—' }}</span>
<span v-if="usesAppPrice" class="order-detail__chip order-detail__chip--soft">قیمت اپ</span>
<span v-else-if="feeSourceLabel" class="order-detail__chip order-detail__chip--soft">
{{ feeSourceLabel }}
</span>
</p>
</div>
</header>
<section class="order-detail__stats">
<article class="order-detail__stat order-detail__stat--accent">
<span class="order-detail__stat-label">{{ paymentLabel }}</span>
<strong class="order-detail__stat-value">{{ formatOrderRial(paymentAmount) }}</strong>
<span class="order-detail__stat-unit">ریال</span>
</article>
<article class="order-detail__stat">
<span class="order-detail__stat-label">مقدار</span>
<strong class="order-detail__stat-value">{{ amountLabel }}</strong>
</article>
<article class="order-detail__stat">
<span class="order-detail__stat-label">جمع سود</span>
<strong class="order-detail__stat-value">{{ formatOrderRial(grandTotal) }}</strong>
<span class="order-detail__stat-unit">ریال</span>
</article>
</section>
<section class="order-detail__receipt">
<div class="order-detail__receipt-head">
<h3 class="order-detail__receipt-title m-0">حساب سود</h3>
<p class="order-detail__receipt-eq m-0">{{ equationHint }}</p>
</div>
<div class="order-detail__receipt-lines">
<div class="order-detail__receipt-line order-detail__receipt-line--base">
<div class="order-detail__receipt-line-main">
<span class="order-detail__receipt-op" aria-hidden="true"> </span>
<div class="min-w-0 flex-1">
<strong class="order-detail__receipt-label">بدون سود</strong>
<p class="order-detail__receipt-detail m-0">
{{ formatOrderRial(order.price_original ?? 0) }} ریال / گرم × {{ amountLabel }}
</p>
</div>
<strong class="order-detail__receipt-amount">
{{ formatOrderRial(baseTotal) }}
<span class="order-detail__stat-unit">ریال</span>
</strong>
</div>
</div>
<template v-if="receiptLines.length">
<div
v-for="(line, idx) in receiptLines"
:key="`${line.key ?? line.kind}-${idx}`"
class="order-detail__receipt-line"
>
<div class="order-detail__receipt-line-main">
<span class="order-detail__receipt-op" aria-hidden="true">{{ profitSign }}</span>
<div class="min-w-0 flex-1">
<strong class="order-detail__receipt-label">{{ line.label || 'سود' }}</strong>
<p class="order-detail__receipt-detail m-0">{{ line.line_text || line.detail || '—' }}</p>
</div>
<strong class="order-detail__receipt-amount">
{{ formatOrderRial(line.total ?? 0) }}
<span class="order-detail__stat-unit">ریال</span>
</strong>
</div>
</div>
</template>
<p v-else class="order-detail__receipt-empty m-0">
اقلام سود برای این سفارش ذخیره نشده — سفارش جدید بعد از آپدیت بزنید.
</p>
<div class="order-detail__receipt-line order-detail__receipt-line--result">
<div class="order-detail__receipt-line-main">
<span class="order-detail__receipt-op" aria-hidden="true">=</span>
<div class="min-w-0 flex-1">
<strong class="order-detail__receipt-label">قیمت مشتری</strong>
<p class="order-detail__receipt-detail m-0">
{{ formatOrderRial(order.price ?? 0) }} ریال / گرم × {{ amountLabel }}
</p>
</div>
<strong class="order-detail__receipt-amount order-detail__receipt-amount--focus">
{{ formatOrderRial(customerTotal) }}
<span class="order-detail__stat-unit">ریال</span>
</strong>
</div>
</div>
</div>
</section>
<section class="order-detail__customer">
<div class="order-detail__customer-grid">
<div class="order-detail__customer-cell">
<span class="order-detail__field-label">کد کاربری</span>
<strong>{{ order.customer?.id ?? '—' }}</strong>
</div>
<div class="order-detail__customer-cell">
<span class="order-detail__field-label">شماره ملی</span>
<strong>{{ order.customer?.n_code || '—' }}</strong>
</div>
<div class="order-detail__customer-cell">
<span class="order-detail__field-label">تلفن</span>
<strong>{{ order.customer?.phone_number || '—' }}</strong>
</div>
<div class="order-detail__customer-cell">
<span class="order-detail__field-label">استان</span>
<strong>{{ order.customer?.province || '—' }}</strong>
</div>
<div class="order-detail__customer-cell">
<span class="order-detail__field-label">شهر</span>
<strong>{{ order.customer?.city || '—' }}</strong>
</div>
</div>
</section>
</div>
</a-modal>
</template>
<script setup lang="ts">
import type { Order } from '@/model/order'
import { isMobile } from '@core/utils/is'
import { toJalaliNoTime, toTime } from '@core/utils/useDataTime'
import { formatAmountWithUnit } from '@/utils/weightUnit'
import {
formatOrderRial,
orderFeeBaseTotal,
orderFeeBreakdown,
orderFeeBreakdownLines,
orderFeeCustomerTotal,
orderFeeGrandTotal,
resolveOrderFeeSourceLabel,
resolveOrderProductTitle
} from '../utils/orderDetailDisplay'
type OrderRow = Order & {
sts_str?: string
transactions?: Array<{
title?: string | null
product_id?: number | null
product?: { id?: number; title?: string | null } | null
}>
}
const props = defineProps<{
visible: boolean
order: OrderRow | null
}>()
const emit = defineEmits<{
(e: 'update:visible', value: boolean): void
(e: 'close'): void
}>()
const visibleProxy = computed({
get: () => props.visible,
set: (value: boolean) => emit('update:visible', value)
})
const modalWidth = computed(() => (isMobile.value ? '96vw' : '72rem'))
const order = computed(() => props.order)
const directionLabel = computed(() => (order.value?.direction === 2 ? 'فروش' : 'خرید'))
const directionClass = computed(() =>
order.value?.direction === 2 ? 'order-detail__badge--sell' : 'order-detail__badge--buy'
)
const dateLabel = computed(() =>
order.value?.created_at ? toJalaliNoTime(order.value.created_at) : '—'
)
const timeLabel = computed(() => (order.value?.created_at ? toTime(order.value.created_at) : ''))
const paymentLabel = computed(() =>
order.value?.direction === 2 ? 'مبلغ دریافتی مشتری' : 'مبلغ پرداختی مشتری'
)
const paymentAmount = computed(() =>
Number(order.value?.payment_amount ?? order.value?.total_price ?? 0)
)
const amountLabel = computed(() => {
if (!order.value) return '—'
return formatAmountWithUnit(order.value.amount_display ?? order.value.amount, order.value.amount_unit)
})
const productTitle = computed(() => (order.value ? resolveOrderProductTitle(order.value) : ''))
const feeSourceLabel = computed(() => (order.value ? resolveOrderFeeSourceLabel(order.value) : ''))
const breakdown = computed(() => (order.value ? orderFeeBreakdown(order.value) : null))
const usesAppPrice = computed(() => Boolean(breakdown.value?.uses_app_price))
const receiptLines = computed(() => (order.value ? orderFeeBreakdownLines(order.value) : []))
const grandTotal = computed(() => (order.value ? orderFeeGrandTotal(order.value) : 0))
const baseTotal = computed(() => (order.value ? orderFeeBaseTotal(order.value) : 0))
const customerTotal = computed(() => (order.value ? orderFeeCustomerTotal(order.value) : 0))
const isSell = computed(() => order.value?.direction === 2)
const profitSign = computed(() => (isSell.value ? '−' : '+'))
const equationHint = computed(() =>
isSell.value
? 'بدون سود − سود = قیمت مشتری'
: 'بدون سود + سود = قیمت مشتری'
)
function close() {
emit('update:visible', false)
emit('close')
}
</script>
<style scoped>
.order-detail {
@apply flex flex-col gap-3 pt-0;
}
.order-detail__muted,
.order-detail__field-label {
color: var(--admin-text-secondary);
}
.order-detail__header {
@apply flex items-start justify-between gap-3 rounded-xl px-4 py-3;
background: var(--admin-stat-muted, rgba(255, 255, 255, 0.04));
}
.order-detail__eyebrow {
@apply text-xs;
color: var(--admin-text-secondary);
}
.order-detail__title {
@apply m-0 text-lg font-bold;
color: var(--admin-text);
}
.order-detail__sub {
@apply m-0 mt-0.5 flex flex-wrap items-center gap-1 text-sm text-primary;
}
.order-detail__dot {
@apply opacity-50;
}
.order-detail__product-line {
@apply mt-2 flex flex-wrap items-center gap-2 text-sm;
}
.order-detail__product-line strong {
@apply font-bold;
color: var(--admin-text);
}
.order-detail__badge {
@apply rounded-lg border px-2 py-0.5 text-xs font-600;
}
.order-detail__badge--buy {
@apply border-green-300/40 text-green-500;
background: rgba(34, 197, 94, 0.12);
}
.order-detail__badge--sell {
@apply border-red-300/40 text-red-400;
background: rgba(248, 113, 113, 0.12);
}
.order-detail__badge--muted {
border-color: var(--admin-border);
color: var(--admin-text-secondary);
background: transparent;
}
.order-detail__stats {
@apply grid grid-cols-1 gap-2 sm:grid-cols-3;
}
.order-detail__stat {
@apply rounded-xl px-4 py-2.5;
border: 1px solid var(--admin-border);
}
.order-detail__stat--accent {
border-color: rgba(34, 197, 94, 0.35);
background: rgba(34, 197, 94, 0.1);
}
.order-detail__stat-label,
.order-detail__field-label {
@apply block text-xs;
}
.order-detail__stat-value {
@apply mt-0.5 block text-lg font-bold;
color: var(--admin-text);
}
.order-detail__stat--accent .order-detail__stat-value {
@apply text-green-500;
}
.order-detail__stat-unit {
@apply text-xs;
color: var(--admin-text-secondary);
}
.order-detail__chip {
@apply rounded-lg px-2 py-0.5 text-xs font-600;
border: 1px solid var(--admin-border);
color: var(--admin-text);
}
.order-detail__chip--soft {
color: var(--admin-text-secondary);
font-weight: 500;
max-width: 18rem;
}
.order-detail__receipt {
@apply rounded-xl px-4 py-3;
border: 1px solid var(--admin-border);
background: var(--admin-stat-muted, rgba(255, 255, 255, 0.03));
}
.order-detail__receipt-head {
@apply mb-3;
}
.order-detail__receipt-title {
@apply text-sm font-bold;
color: var(--admin-text);
}
.order-detail__receipt-eq {
@apply mt-0.5 text-xs;
color: var(--admin-text-secondary);
}
.order-detail__receipt-lines {
@apply flex flex-col gap-2;
}
.order-detail__receipt-line {
@apply rounded-lg px-3 py-2;
border: 1px solid var(--admin-border);
}
.order-detail__receipt-line--base {
background: var(--admin-stat-muted, rgba(255, 255, 255, 0.02));
}
.order-detail__receipt-line--result {
border-color: rgba(34, 197, 94, 0.35);
background: rgba(34, 197, 94, 0.08);
}
.order-detail__receipt-line-main {
@apply flex items-start gap-2;
}
.order-detail__receipt-op {
@apply mt-0.5 w-3 shrink-0 text-center text-sm font-bold text-primary;
}
.order-detail__receipt-label {
@apply block text-sm;
color: var(--admin-text);
}
.order-detail__receipt-detail {
@apply mt-0.5 text-xs leading-relaxed;
color: var(--admin-text-secondary);
}
.order-detail__receipt-amount {
@apply shrink-0 text-sm font-bold;
color: var(--admin-text);
font-variant-numeric: tabular-nums;
}
.order-detail__receipt-amount--focus {
@apply text-green-500;
}
.order-detail__receipt-empty {
@apply text-xs;
color: var(--admin-text-secondary);
}
.order-detail__customer {
@apply rounded-xl px-4 py-2.5;
border: 1px solid var(--admin-border);
}
.order-detail__customer-grid {
@apply grid grid-cols-2 gap-x-4 gap-y-2 md:grid-cols-3 lg:grid-cols-5;
}
.order-detail__customer-cell strong {
@apply mt-0.5 block text-sm font-600;
color: var(--admin-text);
}
</style>
<style>
.order-detail-modal.ant-modal {
max-width: min(72rem, 96vw);
}
.order-detail-modal .ant-modal-body {
padding: 0.75rem 1.25rem 1rem;
}
</style>
@@ -36,17 +36,40 @@
:label="row.direction === 2 ? 'مبلغ دریافتی مشتری (ریال)' : 'مبلغ پرداختی مشتری (ریال)'"
:value="usePrice(row.payment_amount ?? row.total_price ?? 0, true, '')"
/>
<MobileStatCard bordered label="کارمزد (طلا)" :value="serviceChargeLabel(row)" />
<MobileStatCard bordered label="کارمزد (ریال)" :value="usePrice(row.service_charge_price ?? 0, true, '')" />
<MobileStatCard bordered label="قیمت کل بدون سود (ریال)" :value="usePrice(row.total_price_original ?? 0, true, '')" />
<MobileStatCard bordered label="سود واحد (ریال)" :value="usePrice(row.fee ?? 0, true, '')" />
<MobileStatCard
bordered
variant="warning"
label="جمع سود معامله (ریال)"
:value="usePrice(orderFeeGrandTotal(row), true, '')"
/>
</div>
<div v-if="orderFeeBreakdownLines(row).length" class="space-y-2 rounded-xl border border-[var(--admin-border)] p-3">
<p class="m-0 text-sm font-bold">فاکتور سود</p>
<div
v-for="(line, idx) in orderFeeBreakdownLines(row)"
:key="`${line.key ?? line.kind}-${idx}`"
class="text-xs"
>
<p class="m-0 font-600">+ {{ line.label }}</p>
<p class="m-0 mt-0.5 opacity-70">{{ line.line_text || line.detail }}</p>
<p class="m-0 mt-0.5 font-bold">{{ usePrice(Number(line.total ?? 0), true, '') }} ریال</p>
</div>
</div>
<MobileInfoGrid>
<MobileDetailRow label="محصول" :value="productTitle(row) || '-'" />
<MobileDetailRow label="نوع فلز" :value="row.order_metal_label ?? '-'" />
<MobileDetailRow label="منبع اختلاف قیمت" :value="feeSourceLabel(row) || '-'" />
<MobileDetailRow label="کد سفارش" :value="row.id ?? '-'" />
<MobileDetailRow label="شماره موبایل" :value="row.customer?.mobile || '-'" />
<MobileDetailRow label="سود کل (ریال)" :value="usePrice(row.total_fee ?? 0, true, '')" />
<MobileDetailRow
label="سود اختلاف قیمت (ریال)"
:value="usePrice(orderProfitTotal(row), true, '')"
/>
<MobileDetailRow label="کارمزد (طلا)" :value="serviceChargeLabel(row)" />
<MobileDetailRow label="کارمزد (ریال)" :value="usePrice(row.service_charge_price ?? 0, true, '')" />
<MobileDetailRow label="کد کاربری" :value="row.customer?.id ?? '-'" />
<MobileDetailRow label="شماره ملی" :value="row.customer?.n_code || '-'" />
<MobileDetailRow label="شماره تلفن" :value="row.customer?.phone_number || '-'" />
@@ -68,12 +91,24 @@ import { isMobile } from '@core/utils/is'
import { toJalali } from '@core/utils/useDataTime'
import { usePrice } from '@core/utils/usePrice'
import { formatAmountWithUnit } from '@/utils/weightUnit'
import {
orderFeeBreakdownLines,
orderFeeGrandTotal,
orderProfitTotal,
resolveOrderFeeSourceLabel,
resolveOrderProductTitle
} from '../utils/orderDetailDisplay'
type OrderRow = Order & {
sts_str?: string
service_charge?: number | string
service_charge_price?: number | string
payment_amount?: number | string
transactions?: Array<{
title?: string | null
product_id?: number | null
product?: { id?: number; title?: string | null } | null
}>
}
const props = defineProps<{
@@ -130,4 +165,12 @@ function serviceChargeLabel(row: OrderRow) {
if (Number.isNaN(value)) return String(row.service_charge)
return formatAmountWithUnit(value, row.amount_unit)
}
function productTitle(row: OrderRow) {
return resolveOrderProductTitle(row)
}
function feeSourceLabel(row: OrderRow) {
return resolveOrderFeeSourceLabel(row)
}
</script>
+5 -104
View File
@@ -54,15 +54,10 @@
@calculate-cash="calculateCash"
/>
</BasicActionSheet>
<BasicInfoModal
<OrderDetailModal
v-model:visible="userInfoVisible"
:order="selectedOrderInfo"
@close="userInfoVisible = false"
:title="infoParams.title"
:date="infoParams.date"
:time="infoParams.time"
:type="infoParams.type"
:name="infoParams.name"
:items="infoParams.items"
/>
</div>
</template>
@@ -73,24 +68,19 @@ import { useStaticApiListsStore } from '@/store/staticApisStore'
import { ref } from 'vue'
import tbl from './components/table.vue'
import type { FormInstance } from 'ant-design-vue'
import BasicInfoModal from '@/components/BasicInfoModal/index.vue'
import BasicActionSheet from '@/components/BasicActionSheet.vue'
import OrderEditorFormBody from './components/OrderEditorFormBody.vue'
import OrderDetailModal from './components/OrderDetailModal.vue'
import { message } from 'ant-design-vue/es'
import type { Info } from '@/components/BasicInfoModal/model/Info'
import { toJalaliNoTime, toTime } from '@core/utils/useDataTime'
import { confirmDeleteItem } from '@/utils/confirmModal'
import { cloneDeep } from 'lodash'
import type { Customer } from '@/model/customer'
import { deleteCustomer, getCustomersList } from '@/api/customer'
import type { Order } from '@/model/order'
import { usePrice } from '@core/utils/usePrice'
import { getLastPrice } from '@/api/price'
import type { last } from '@/model/price'
import { createOrder } from '@/api/order'
import { isMobile } from '@core/utils/is'
import { metalLabel } from '@/utils/adminMetalLabels'
import { formatAmountWithUnit } from '@/utils/weightUnit'
import { initWalletSettingsFromApi } from '@/utils/config'
//loading
@@ -198,99 +188,10 @@ function stopGetPrice() {
}
//info
let infoParams = reactive<Info>({})
const selectedOrderInfo = ref<Order | null>(null)
function info(val: Order) {
infoParams.name = val.customer.name
infoParams.type = val.customer.mobile
infoParams.date = toJalaliNoTime(val.created_at)
infoParams.time = toTime(val.created_at)
infoParams.itemsTitle = 'اطلاعات کاربری'
infoParams.items = [
{
key: 'مقدار',
value: formatAmountWithUnit(val.amount_display ?? val.amount, val.amount_unit)
},
{
key: 'نوع فلز',
value: val.order_metal_label ?? '-'
},
{
key: 'قیمت زمان معامله (ریال)',
value: usePrice(val.price ?? 0) ?? '-'
},
{
key: 'قیمت کل برای مشتری (ریال)',
value: usePrice(val.total_price ?? 0) ?? '-'
},
{
key: metalLabel.goldGramPriceOriginal(),
value: usePrice(val.price_original ?? 0) ?? '-'
},
{
key: 'قیمت کل بدون سود (ریال)',
value: usePrice(val.total_price_original ?? 0) ?? '-'
},
/* {
key: 'قیمت کل لیمان (ریال)',
value: usePrice(val.total_price_liman ?? 0) ?? '-'
},*/
{
key: 'سود هر گرم (ریال)',
value: usePrice(val.fee ?? 0) ?? '-'
},
{
key: 'سود کل (ریال)',
value: usePrice(val.total_fee ?? 0) ?? '-'
},
{
key: 'کد کاربری',
value: val.customer.id ?? '-'
},
{
key: 'شماره ملی',
value: val.customer.n_code ?? '-'
},
{
key: 'کارمزد (طلا)',
value:
val.service_charge != null && val.service_charge !== ''
? formatAmountWithUnit(Number(val.service_charge), val.amount_unit)
: '-'
},
{
key: 'کارمزد (ریال)',
value: usePrice(Number(val.service_charge_price ?? 0)) ?? '-'
},
{
key: val.direction === 2 ? 'مبلغ دریافتی مشتری (ریال)' : 'مبلغ پرداختی مشتری (ریال)',
value: usePrice(val.payment_amount ?? val.total_price ?? 0) ?? '-'
},
/* {
key: 'تاریخ تولد',
value: toJalaliNoTime(val.customer.birth_date) ?? '-'
},*/
/* {
key: 'نام فروشگاه',
value: val.customer.shop_name ?? '-'
},*/
{
key: 'شماره تلفن',
value: val.customer.phone_number ?? '-'
},
{
key: 'نام استان',
value: val.customer.province ?? '-'
},
{
key: 'نام شهر',
value: val.customer.city ?? '-'
}
/* {
key: 'نام چاپی',
value: val.print_name ?? '-'
}*/
]
selectedOrderInfo.value = val
userInfoVisible.value = true
}
@@ -0,0 +1,106 @@
import type { Order, OrderFeeBreakdown, OrderFeeBreakdownLine } from '@/model/order'
type TxLike = {
title?: string | null
product_id?: number | null
product?: { id?: number; title?: string | null } | null
}
export function resolveOrderProductTitle(order: Order & { transactions?: TxLike[] }): string {
const direct = (order.product_title ?? '').trim()
if (direct) return direct
const txs = order.transactions ?? []
for (const tx of txs) {
const fromProduct = (tx.product?.title ?? '').trim()
if (fromProduct) return fromProduct
}
for (const tx of txs) {
const title = (tx.title ?? '').trim()
if (title.startsWith('خرید ')) return title.slice(5).trim()
if (title.startsWith('فروش ')) return title.slice(5).trim()
}
return ''
}
export function resolveOrderFeeSourceLabel(order: Order): string {
const labeled = (order.fee_source_label ?? '').trim()
if (labeled) return labeled
if (order.fee_source === 'app+product') {
return order.direction === 2
? 'قیمت اپ (sell_fee) + تب اختلاف محصول'
: 'قیمت اپ (buy_fee) + تب اختلاف محصول'
}
if (order.fee_source === 'settings') {
const key = order.fee_source_key || (order.direction === 2 ? 'sell_fee' : 'buy_fee')
return order.direction === 2
? `تنظیمات سراسری · کسر قیمت فروش (${key})`
: `تنظیمات سراسری · اضافه قیمت خرید (${key})`
}
if (order.fee_source === 'product') {
return order.direction === 2
? 'تب اختلاف محصول · هزینه فروش (cash_fee_sell)'
: 'تب اختلاف محصول · هزینه خرید (cash_fee_buy)'
}
const per = orderProfitPerUnit(order)
if (per > 0) {
return `اختلاف قیمت ذخیره‌شده روی سفارش (${per.toLocaleString('fa-IR')} ریال/گرم)`
}
return ''
}
export function orderProfitPerUnit(order: Order): number {
return Math.abs(Number(order.fee_display ?? order.fee ?? 0))
}
export function orderProfitTotal(order: Order): number {
return Math.abs(Number(order.total_fee_display ?? order.total_fee ?? 0))
}
export function orderFeeBreakdown(order: Order): OrderFeeBreakdown | null {
const raw = order.fee_breakdown
if (!raw || typeof raw !== 'object') return null
return raw
}
export function orderFeeBreakdownLines(order: Order): OrderFeeBreakdownLine[] {
return orderFeeBreakdown(order)?.lines ?? []
}
export function orderFeeGrandTotal(order: Order): number {
const breakdown = orderFeeBreakdown(order)
if (breakdown && breakdown.grand_total != null) {
return Math.round(Math.abs(Number(breakdown.grand_total)))
}
const spread = orderProfitTotal(order)
const service = Math.abs(Number(order.service_charge_price ?? 0))
return Math.round(spread + service)
}
export function orderFeeBaseTotal(order: Order): number {
const breakdown = orderFeeBreakdown(order)
if (breakdown?.base_total != null) {
return Math.round(Math.abs(Number(breakdown.base_total)))
}
return Math.round(Math.abs(Number(order.total_price_original ?? 0)))
}
export function orderFeeCustomerTotal(order: Order): number {
const breakdown = orderFeeBreakdown(order)
if (breakdown?.customer_total != null) {
return Math.round(Math.abs(Number(breakdown.customer_total)))
}
return Math.round(Math.abs(Number(order.total_price ?? 0)))
}
/** مبالغ ریالی — بدون اعشار تا usePrice ویرگول اشتباه نگذارد */
export function formatOrderRial(value: number | string | null | undefined): string {
const n = Math.round(Math.abs(Number(value ?? 0)))
if (!Number.isFinite(n)) return '0'
return n.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',')
}