fix edits for borsemes

This commit is contained in:
2026-07-23 17:53:28 +03:30
parent 73feee2048
commit d505f3b5bd
12 changed files with 353 additions and 261 deletions
+3
View File
@@ -8,3 +8,6 @@ export const changeAccountSts = async (obj: cardSts) =>
export const getAccountTable = async (params: TableInput) =>
await useAxios.post<Array<Card>>('/admin/account/table', params)
export const deleteAccount = async (id: number) =>
await useAxios.delete<boolean>(`/admin/account/${id}`)
+4
View File
@@ -78,6 +78,10 @@ export interface Order {
service_charge_price?: number | string
/** درصد کارمزد اعمال‌شده */
service_charge_percentage?: number | string
/**
* سمت کسر کارمزد: gold = از کیف فلز/مقداری، rial = از ریال
*/
fee_asset?: 'gold' | 'rial' | string | null
/** مبلغ نقدی جابه‌جاشده: خرید = پرداختی مشتری، فروش = دریافتی مشتری */
payment_amount?: number
}
+36 -7
View File
@@ -10,16 +10,18 @@
<div class="flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1">
<DashboardLabelWithHelp
label="مظنه"
:help="copy.hero"
:help="heroHelp"
size="xs"
text-class="text-[11px]"
label-class="text-white/80"
/>
<span class="text-[11px] text-white/80">/گرم</span>
<span class="text-[11px] text-white/80">/{{ quoteUnitLabel }}</span>
<span class="text-lg font-bold leading-none sm:text-xl">{{ usePrice(master?.price ?? 0, true, 'ریال') }}</span>
<span class="text-white/35">·</span>
<span class="text-[11px] text-white/80">مثقال</span>
<span class="text-sm font-semibold leading-none sm:text-base">{{ usePrice(master?.pricem ?? 0, true, 'ریال') }}</span>
<template v-if="showMesghalQuote">
<span class="text-white/35">·</span>
<span class="text-[11px] text-white/80">مثقال</span>
<span class="text-sm font-semibold leading-none sm:text-base">{{ usePrice(master?.pricem ?? 0, true, 'ریال') }}</span>
</template>
</div>
<p v-if="master?.update_date" class="mt-1.5 text-[10px] leading-none text-white/55">
{{ toJalali(master.update_date) }}
@@ -344,7 +346,7 @@
<th class="px-2 py-2 text-right">قیمت مشتری</th>
<th class="px-2 py-2 text-right">سود اختلاف قیمت/گرم</th>
<th class="px-2 py-2 text-right">سود کل اختلاف قیمت</th>
<th class="px-2 py-2 text-right">کارمزد (ریال)</th>
<th class="px-2 py-2 text-right">کارمزد</th>
<th class="px-2 py-2 text-right">وضعیت</th>
<th class="px-2 py-2 text-right">تاریخ</th>
</tr>
@@ -372,7 +374,15 @@
{{ usePrice(order.total_fee ?? 0, true, 'ریال') }}
</td>
<td class="px-2 py-2.5">
{{ usePrice(order.service_charge_price ?? 0, true, 'ریال') }}
<div class="leading-snug">
<div>{{ orderServiceFee(order).value }}</div>
<div
v-if="orderServiceFee(order).source"
class="dashboard-text-muted text-[11px]"
>
{{ orderServiceFee(order).source }}
</div>
</div>
</td>
<td class="px-2 py-2.5">
<span class="rounded-md px-2 py-0.5 text-xs" :class="stsClass(order.sts)">
@@ -465,6 +475,9 @@ import { DASHBOARD_RIAL_UNIT, formatDashboardAmount } from '@/pages/dashboard/da
import { useDashboardPage } from '@/pages/dashboard/composables/useDashboardPage'
import { useDashboardQuickActions } from '@/pages/dashboard/composables/useDashboardQuickActions'
import { metalBalanceTitle, metalLabel } from '@/utils/adminMetalLabels'
import { formatOrderServiceFee } from '@/utils/orderServiceFee'
import { getMainMetal } from '@/utils/metalLabels'
import { TRADE_WEIGHT_UNIT_GRAM, TRADE_WEIGHT_UNIT_KILOGRAM } from '@/utils/weightUnit'
const copy = {
hero: dashboardHero,
@@ -523,6 +536,22 @@ const {
customerStats,
} = useDashboardPage()
const orderServiceFee = formatOrderServiceFee
const isCopperMainMetal = computed(() => getMainMetal().trim() === 'مس')
const quoteUnitLabel = computed(() =>
isCopperMainMetal.value ? TRADE_WEIGHT_UNIT_KILOGRAM : TRADE_WEIGHT_UNIT_GRAM
)
const showMesghalQuote = computed(() => !isCopperMainMetal.value)
const heroHelp = computed(() =>
isCopperMainMetal.value
? {
description: 'آخرین قیمت پایه هر کیلوگرم مس در سیستم.',
formula: 'مظنه بر حسب کیلوگرم نمایش داده می‌شود؛ مثقال برای مس کاربرد ندارد.',
}
: copy.hero
)
const tradingProfitTotal = computed(() => {
const priceDiff = Number(finance.value?.fullPriceSood ?? 0)
const commission = Number(finance.value?.fullPriceKarmozd ?? finance.value?.fullPriceKaermozd ?? 0)
@@ -17,6 +17,7 @@
:item="item"
@open="openSheet"
@toggle-sts="confirmToggleSts"
@delete="confirmDelete"
/>
</MobileCardList>
@@ -37,6 +38,7 @@ type AccountRow = Card & { sts_str?: string; created_at?: string; customer_id?:
const emit = defineEmits<{
(e: 'toggleSts', item: AccountRow): void
(e: 'delete', id: number): void
}>()
const { sheetVisible, selectedItem, openSheet, syncSelectedItem } = useMobileDetailSheet<AccountRow>()
@@ -55,6 +57,11 @@ function confirmToggleSts(item: AccountRow) {
})
}
function confirmDelete(item: AccountRow) {
if (item.id == null) return
emit('delete', item.id)
}
const { loading, search, items, hasMore, reload, loadMore, fetch } = useMobileTableList<AccountRow>({
fetchApi: getAccountTable,
onFetched: (rows) => syncSelectedItem(rows)
@@ -20,6 +20,12 @@
:size="20"
@click="$emit('toggleSts', item)"
/>
<MobileIconButton
icon="vuesax-linear:trash"
color="#DC2626"
:size="20"
@click="$emit('delete', item)"
/>
</div>
</MobileCard>
</template>
@@ -38,6 +44,7 @@ const props = defineProps<{
defineEmits<{
(e: 'open', item: AccountRow): void
(e: 'toggleSts', item: AccountRow): void
(e: 'delete', item: AccountRow): void
}>()
const customerName = computed(() => props.item.customer?.name || '-')
@@ -10,7 +10,7 @@
</BaseButton>-->
</template>
<template #action="{ column, record }">
<div class="gap-2 flex justify-center">
<div class="gap-2 flex justify-center items-center">
<a-tooltip
:key="record.sts"
placement="bottom"
@@ -25,6 +25,12 @@
:color="record.sts !== 2 ? 'red' : 'green'"
/>
</a-tooltip>
<a-tooltip placement="bottom" class="cursor-pointer" @click="emits('delete', record.id)">
<template #title>
<span>حذف کارت</span>
</template>
<Icon icon="vuesax-linear:trash" color="red" />
</a-tooltip>
</div>
</template>
</BaseTable>
@@ -34,6 +40,7 @@
ref="mobileListRef"
class="min-h-0 flex-1"
@toggle-sts="changeAccountStsFunction"
@delete="(id) => emits('delete', id)"
/>
</div>
</BasicShowContext>
+3 -202
View File
@@ -2,220 +2,21 @@
<div>
<BasicMainCard>
<div class="flex w-full h-full">
<tbl
ref="table"
class="w-full h-full"
@edit="edit"
@delete="deleteItem"
@withdraw="openWithdraw"
/>
<!-- <BasicMenu :menu="menu" width="13rem" @onChange="tabSelected" />
<component :is="selectedComponent" class="mr-5" />-->
<!-- <BasicShowContext title="پروژه ها"> <Search /> </BasicShowContext>-->
<tbl ref="table" class="w-full h-full" @delete="deleteItem" />
</div>
</BasicMainCard>
<!-- <BasicInfoModal
v-model:visible="userInfoVisible"
@close="userInfoVisible = false"
:title="infoParams.title"
:date="infoParams.date"
:time="infoParams.time"
:type="infoParams.type"
:name="infoParams.name"
:items="infoParams.items"
/>-->
</div>
</template>
<script setup lang="ts">
// import BasicInfoModal from '@/components/BasicInfoModal'
import { inputLenRules, inputRules } from '@/utils/inputRules'
import { useStaticApiListsStore } from '@/store/staticApisStore'
import { ref } from 'vue'
import { DatePicker } from 'ant-design-vue'
import tbl from './components/table.vue'
import type { FormInstance } from 'ant-design-vue'
import BasicInfoModal from '@/components/BasicInfoModal/index.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 { Item } from '@/model/Item'
import type { Customer } from '@/model/customer'
import { createCustomer, deleteCustomer, updateCustomer } from '@/api/customer'
import type { Order } from '@/model/order'
import type { Wallet, Withdraw } from '@/model/wallet'
import { completeWithdraw } from '@/api/wallet'
//import DatePicker from '@core/components/_sample/components/datePicker.vue'
import { deleteAccount } from '@/api/card'
//loading
const loading = ref<boolean>(false)
//define store
const store = useStaticApiListsStore()
//define tbl
const table = ref(null)
const refreshTable = () => table?.value?.fetch()
let updateKey = ref<number>(0)
//form
const form = ref<Withdraw>({})
const formRef = ref<FormInstance>()
//info
let infoParams = reactive<Info>({})
/*
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: val.amount ?? '-'
},
{
key: 'قیمت هر گرم طلا برای مشتری (ریال)',
value: val.price ?? '-'
},
{
key: 'قیمت کل برای مشتری (ریال)',
value: val.total_price ?? '-'
},
{
key: '/قیمت هر گرم طلا بدون سود (ریال)',
value: val.price_original ?? '-'
},
{
key: 'قیمت کل بدون سود (ریال)',
value: val.total_price_original ?? '-'
},
{
key: 'سود هر گرم (ریال)',
value: val.fee ?? '-'
},
{
key: 'سود کل (ریال)',
value: val.total_fee ?? '-'
},
{
key: 'کد کاربری',
value: val.customer.id ?? '-'
},
{
key: 'شماره ملی',
value: val.customer.n_code ?? '-'
},
{
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 ?? '-'
}*!/
]
userInfoVisible.value = true
}
*/
//edit
let editId: number | undefined = undefined
async function edit(record: Customer) {
confirmWithdrawVisible.value = true
editId = record.id
form.value = cloneDeep(record)
}
//add
/*async function add() {
loading.value = true
editId = undefined
// await store.getUsersList()
confirmWithdrawVisible.value = true
updateKey.value++
resetData()
loading.value = false
}*/
function openWithdraw(val: number) {
confirmWithdrawId = val
confirmWithdrawVisible.value = true
}
//reset data
const resetData = () => {
formRef.value?.resetFields()
form.value = {
tracked_number: undefined
}
}
//delete
const deleteItem = (id: number) => confirmDeleteItem('کاربر', id, deleteCustomer, refreshTable)
//define selects
onMounted(async () => {
loading.value = true
// await getResponsible()
// await getQC()
// await getManager()
loading.value = false
})
//add project modal
const confirmWithdrawVisible = ref<boolean>(false)
let confirmWithdrawId: number
// const userInfoVisible = ref<boolean>(false)
//submit user
async function okWithdraw() {
formRef.value?.validateFields().then(async () => {
// console.log('salam', form.value)
// return
loading.value = true
try {
form.value.id = confirmWithdrawId
await completeWithdraw(form.value)
confirmWithdrawVisible.value = false
refreshTable()
resetData()
message.success('شماره پیگیری با موفقیت تایید شد')
// await getResponsible()
// await getQC()
// await getManager()
} catch (e) {
console.error(e)
// message.error('خطا در بارگذاری اطلاعات')
} finally {
loading.value = false
}
})
}
const deleteItem = (id: number) => confirmDeleteItem('کارت بانکی', id, deleteAccount, refreshTable)
</script>
<style scoped></style>
@@ -31,10 +31,6 @@
<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>
@@ -53,13 +49,17 @@
<span class="order-detail__stat-label">جمع سود</span>
<strong class="order-detail__stat-value">{{ formatOrderRial(grandTotal) }}</strong>
<span class="order-detail__stat-unit">ریال</span>
<span class="order-detail__stat-hint">اختلاف قیمت + کارمزد</span>
</article>
</section>
<section class="order-detail__receipt">
<div class="order-detail__receipt-head">
<h3 class="order-detail__receipt-title m-0">حساب سود</h3>
<h3 class="order-detail__receipt-title m-0">سود اختلاف قیمت</h3>
<p class="order-detail__receipt-eq m-0">{{ equationHint }}</p>
<p v-if="feeSourceLabel" class="order-detail__receipt-source m-0">
منبع اختلاف: {{ feeSourceLabel }}
</p>
</div>
<div class="order-detail__receipt-lines">
@@ -69,7 +69,7 @@
<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 }}
{{ formatOrderRial(order.price_original ?? 0) }} {{ perUnitLabel }} × {{ amountLabel }}
</p>
</div>
<strong class="order-detail__receipt-amount">
@@ -79,16 +79,16 @@
</div>
</div>
<template v-if="receiptLines.length">
<template v-if="spreadLines.length">
<div
v-for="(line, idx) in receiptLines"
v-for="(line, idx) in spreadLines"
: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>
<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">
@@ -99,7 +99,7 @@
</div>
</template>
<p v-else class="order-detail__receipt-empty m-0">
اقلام سود برای این سفارش ذخیره نشده — سفارش جدید بعد از آپدیت بزنید.
سود اختلاف قیمت برای این سفارش ثبت نشده است.
</p>
<div class="order-detail__receipt-line order-detail__receipt-line--result">
@@ -108,7 +108,7 @@
<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 }}
{{ formatOrderRial(order.price ?? 0) }} {{ perUnitLabel }} × {{ amountLabel }}
</p>
</div>
<strong class="order-detail__receipt-amount order-detail__receipt-amount--focus">
@@ -120,6 +120,48 @@
</div>
</section>
<section class="order-detail__receipt order-detail__receipt--fee">
<div class="order-detail__receipt-head">
<h3 class="order-detail__receipt-title m-0">کارمزد معامله</h3>
<p class="order-detail__receipt-eq m-0">
فقط از درصد کارمزد سرویس — جدا از سود اختلاف قیمت
</p>
</div>
<div class="order-detail__receipt-lines">
<div class="order-detail__receipt-line">
<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">{{ serviceFee.label }}</strong>
<p class="order-detail__receipt-detail m-0">{{ serviceFee.detail }}</p>
<span
v-if="serviceFee.source"
class="order-detail__chip order-detail__chip--fee mt-1 inline-flex"
>
کسر {{ serviceFee.source }}
</span>
<span
v-else-if="serviceFee.total <= 0"
class="order-detail__chip order-detail__chip--soft mt-1 inline-flex"
>
کارمزدی کسر نشده
</span>
</div>
<strong class="order-detail__receipt-amount">
<template v-if="serviceFee.source && serviceFee.value !== '۰'">
{{ serviceFee.value }}
</template>
<template v-else>
{{ formatOrderRial(serviceFee.total) }}
<span class="order-detail__stat-unit">ریال</span>
</template>
</strong>
</div>
</div>
</div>
</section>
<section class="order-detail__customer">
<div class="order-detail__customer-grid">
<div class="order-detail__customer-cell">
@@ -156,10 +198,11 @@ import { formatAmountWithUnit } from '@/utils/weightUnit'
import {
formatOrderRial,
orderFeeBaseTotal,
orderFeeBreakdown,
orderFeeBreakdownLines,
orderFeeCustomerTotal,
orderFeeGrandTotal,
orderPricePerUnitLabel,
orderServiceChargeDisplay,
orderSpreadProfitLines,
resolveOrderFeeSourceLabel,
resolveOrderProductTitle
} from '../utils/orderDetailDisplay'
@@ -216,18 +259,22 @@ const amountLabel = computed(() => {
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 spreadLines = computed(() => (order.value ? orderSpreadProfitLines(order.value) : []))
const serviceFee = computed(() =>
order.value
? orderServiceChargeDisplay(order.value)
: { total: 0, label: 'کارمزد معامله', detail: '—', source: '', 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 perUnitLabel = computed(() => (order.value ? orderPricePerUnitLabel(order.value) : 'ریال / گرم'))
const isSell = computed(() => order.value?.direction === 2)
const profitSign = computed(() => (isSell.value ? '−' : '+'))
const equationHint = computed(() =>
isSell.value
? 'بدون سود − سود = قیمت مشتری'
: 'بدون سود + سود = قیمت مشتری'
? 'بدون سود − سود اختلاف قیمت = قیمت مشتری'
: 'بدون سود + سود اختلاف قیمت = قیمت مشتری'
)
function close() {
@@ -331,6 +378,11 @@ function close() {
color: var(--admin-text-secondary);
}
.order-detail__stat-hint {
@apply mt-0.5 block text-[11px];
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);
@@ -340,7 +392,12 @@ function close() {
.order-detail__chip--soft {
color: var(--admin-text-secondary);
font-weight: 500;
max-width: 18rem;
}
.order-detail__chip--fee {
@apply text-primary;
border-color: var(--admin-border);
background: var(--admin-stat-muted, rgba(255, 255, 255, 0.04));
}
.order-detail__receipt {
@@ -363,6 +420,10 @@ function close() {
color: var(--admin-text-secondary);
}
.order-detail__receipt-source {
@apply mt-1 text-xs text-primary;
}
.order-detail__receipt-lines {
@apply flex flex-col gap-2;
}
@@ -45,31 +45,45 @@
/>
</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 class="space-y-2 rounded-xl border border-[var(--admin-border)] p-3">
<p class="m-0 text-sm font-bold">سود اختلاف قیمت</p>
<p v-if="feeSourceLabel(row)" class="m-0 text-xs opacity-70">منبع: {{ feeSourceLabel(row) }}</p>
<template v-if="spreadLines(row).length">
<div
v-for="(line, idx) in spreadLines(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>
</template>
<p v-else class="m-0 text-xs opacity-70">سود اختلاف قیمت ثبت نشده است.</p>
</div>
<div class="space-y-2 rounded-xl border border-[var(--admin-border)] p-3">
<p class="m-0 text-sm font-bold">کارمزد معامله</p>
<p class="m-0 text-xs opacity-70">جدا از سود اختلاف قیمت · فقط درصد کارمزد سرویس</p>
<MobileDetailRow label="مبلغ" :value="serviceFeeDisplay(row).value" />
<MobileDetailRow
v-if="serviceFeeDisplay(row).source"
label="کسر از"
:value="serviceFeeDisplay(row).source"
/>
<p v-else class="m-0 text-xs opacity-70">کارمزدی کسر نشده</p>
</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(orderProfitTotal(row), true, '')"
:value="usePrice(spreadTotal(row), true, '')"
/>
<MobileDetailRow label="کارمزد (طلا)" :value="serviceChargeLabel(row)" />
<MobileDetailRow label="کارمزد (ریال)" :value="usePrice(row.service_charge_price ?? 0, true, '')" />
<MobileDetailRow label="جمع سود (اختلاف + کارمزد)" :value="usePrice(orderFeeGrandTotal(row), true, '')" />
<MobileDetailRow label="کد کاربری" :value="row.customer?.id ?? '-'" />
<MobileDetailRow label="شماره ملی" :value="row.customer?.n_code || '-'" />
<MobileDetailRow label="شماره تلفن" :value="row.customer?.phone_number || '-'" />
@@ -91,10 +105,10 @@ import { isMobile } from '@core/utils/is'
import { toJalali } from '@core/utils/useDataTime'
import { usePrice } from '@core/utils/usePrice'
import { formatAmountWithUnit } from '@/utils/weightUnit'
import { formatOrderServiceFee } from '@/utils/orderServiceFee'
import {
orderFeeBreakdownLines,
orderFeeGrandTotal,
orderProfitTotal,
orderSpreadProfitLines,
resolveOrderFeeSourceLabel,
resolveOrderProductTitle
} from '../utils/orderDetailDisplay'
@@ -159,11 +173,16 @@ function amountLabel(row: OrderRow) {
return formatAmountWithUnit(row.amount_display ?? row.amount, row.amount_unit)
}
function serviceChargeLabel(row: OrderRow) {
if (row.service_charge == null || row.service_charge === '') return '-'
const value = Number(row.service_charge)
if (Number.isNaN(value)) return String(row.service_charge)
return formatAmountWithUnit(value, row.amount_unit)
function serviceFeeDisplay(row: OrderRow) {
return formatOrderServiceFee(row)
}
function spreadLines(row: OrderRow) {
return orderSpreadProfitLines(row)
}
function spreadTotal(row: OrderRow) {
return orderSpreadProfitLines(row).reduce((sum, line) => sum + Math.abs(Number(line.total ?? 0)), 0)
}
function productTitle(row: OrderRow) {
@@ -3,6 +3,7 @@ import { toJalali } from '@core/utils/useDataTime'
import CustomTag from '@/components/CustomTag.vue'
import { usePrice } from '@core/utils/usePrice'
import { formatAmountWithUnit } from '@/utils/weightUnit'
import { formatOrderServiceFee } from '@/utils/orderServiceFee'
export type OrderMetalColumn = { metal_id: number; label: string; display_unit?: string }
@@ -103,13 +104,18 @@ export function buildOrderColumns(_metals: OrderMetalColumn[] = []): BaseTableCo
title: 'کارمزد',
dataIndex: 'service_charge',
key: 'service_charge',
type: 'Number',
type: 'Text',
customRender({ record }) {
const value = record.service_charge
if (value == null || value === '') {
return '-'
const fee = formatOrderServiceFee(record)
if (!fee.source) {
return fee.value
}
return formatAmountWithUnit(Number(value), record.amount_unit)
return (
<div class="leading-snug text-right">
<div>{fee.value}</div>
<div class="text-[11px] opacity-70">{fee.source}</div>
</div>
)
}
},
{
@@ -1,4 +1,6 @@
import type { Order, OrderFeeBreakdown, OrderFeeBreakdownLine } from '@/model/order'
import { formatOrderServiceFee } from '@/utils/orderServiceFee'
import { formatAmountWithUnit } from '@/utils/weightUnit'
type TxLike = {
title?: string | null
@@ -6,6 +8,15 @@ type TxLike = {
product?: { id?: number; title?: string | null } | null
}
function orderAmountUnits(order: Order): number {
const n = Number(order.amount_display ?? order.amount ?? 0)
return Number.isFinite(n) ? Math.abs(n) : 0
}
function isServiceChargeLine(line: OrderFeeBreakdownLine): boolean {
return line.kind === 'service_charge' || line.key === 'service_charge'
}
export function resolveOrderProductTitle(order: Order & { transactions?: TxLike[] }): string {
const direct = (order.product_title ?? '').trim()
if (direct) return direct
@@ -72,32 +83,126 @@ export function orderFeeBreakdownLines(order: Order): OrderFeeBreakdownLine[] {
return orderFeeBreakdown(order)?.lines ?? []
}
/** اقلام سود اختلاف قیمت (بدون کارمزد سرویس) — با fallback از fee/total_fee یا اختلاف قیمت‌ها */
export function orderSpreadProfitLines(order: Order): OrderFeeBreakdownLine[] {
const fromBreakdown = orderFeeBreakdownLines(order).filter((line) => !isServiceChargeLine(line))
if (fromBreakdown.length) return fromBreakdown
const amount = orderAmountUnits(order)
const unit = (order.amount_unit || 'گرم').trim() || 'گرم'
let perUnit = orderProfitPerUnit(order)
let total = orderProfitTotal(order)
if (perUnit <= 0 && total <= 0) {
const base = orderFeeBaseTotal(order)
const customer = orderFeeCustomerTotal(order)
total = Math.round(Math.abs(customer - base))
perUnit = amount > 0 ? Math.round(total / amount) : 0
}
if (perUnit <= 0 && total <= 0) return []
const detail =
perUnit > 0 && amount > 0
? `${formatOrderRial(perUnit)} ریال / ${unit} × ${formatAmountWithUnit(amount, unit)}`
: 'از فیلدهای ذخیره‌شده سفارش'
return [
{
kind: 'spread',
key: order.fee_source_key || 'fee',
label: 'سود اختلاف قیمت',
per_unit: perUnit,
amount_units: amount,
total,
detail,
line_text: `${detail} = ${formatOrderRial(total)} ریال`,
},
]
}
/** کارمزد سرویس با منبع کسر (ریال / فلز) */
export function orderServiceChargeDisplay(order: Order): {
total: number
label: string
detail: string
source: string
value: string
} {
const fee = formatOrderServiceFee(order)
const total = Math.round(Math.abs(Number(order.service_charge_price ?? 0)))
const pct = Number(order.service_charge_percentage ?? 0)
const fromLine = orderFeeBreakdownLines(order).find(isServiceChargeLine)
const detailParts: string[] = []
if (fromLine?.line_text || fromLine?.detail) {
detailParts.push(String(fromLine.line_text || fromLine.detail))
} else {
if (Number.isFinite(pct) && pct > 0) {
detailParts.push(`${pct.toLocaleString('fa-IR', { maximumFractionDigits: 4 })}٪ از fee_service_charge_percentage`)
}
if (fee.value && fee.value !== '۰') detailParts.push(fee.value)
}
if (fee.source) detailParts.push(fee.source)
return {
total,
label: fee.source ? `کارمزد معامله · ${fee.source}` : 'کارمزد معامله',
detail: detailParts.filter(Boolean).join(' · ') || '—',
source: fee.source,
value: fee.value,
}
}
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 fromBreakdown = Math.round(Math.abs(Number(breakdown.grand_total)))
if (fromBreakdown > 0) return fromBreakdown
}
const spread = orderProfitTotal(order)
const spreadLines = orderSpreadProfitLines(order)
const spread = spreadLines.reduce((sum, line) => sum + Math.abs(Number(line.total ?? 0)), 0)
const service = Math.abs(Number(order.service_charge_price ?? 0))
return Math.round(spread + service)
}
function looksLikeUnitPriceAsTotal(total: number, perUnit: number, amount: number): boolean {
return amount > 1 && perUnit > 0 && total === Math.round(perUnit)
}
export function orderFeeBaseTotal(order: Order): number {
const amount = orderAmountUnits(order)
const perUnit = Math.abs(Number(order.price_original ?? 0))
const computed = amount > 0 && perUnit > 0 ? Math.round(amount * perUnit) : 0
const breakdown = orderFeeBreakdown(order)
if (breakdown?.base_total != null) {
return Math.round(Math.abs(Number(breakdown.base_total)))
const fromBreakdown = Math.round(Math.abs(Number(breakdown.base_total)))
if (fromBreakdown > 0 && !looksLikeUnitPriceAsTotal(fromBreakdown, perUnit, amount)) {
return fromBreakdown
}
}
return Math.round(Math.abs(Number(order.total_price_original ?? 0)))
const stored = Math.round(Math.abs(Number(order.total_price_original ?? 0)))
// اگر فقط مظنهٔ واحد ذخیره شده (نه جمع)، از حاصل‌ضرب استفاده کن
if (stored > 0 && !looksLikeUnitPriceAsTotal(stored, perUnit, amount)) {
return stored
}
return computed || stored
}
export function orderFeeCustomerTotal(order: Order): number {
const breakdown = orderFeeBreakdown(order)
if (breakdown?.customer_total != null) {
return Math.round(Math.abs(Number(breakdown.customer_total)))
const fromBreakdown = Math.round(Math.abs(Number(breakdown.customer_total)))
if (fromBreakdown > 0) return fromBreakdown
}
return Math.round(Math.abs(Number(order.total_price ?? 0)))
}
export function orderPricePerUnitLabel(order: Order): string {
const unit = (order.amount_unit || 'گرم').trim() || 'گرم'
return `ریال / ${unit}`
}
/** مبالغ ریالی — بدون اعشار تا usePrice ویرگول اشتباه نگذارد */
export function formatOrderRial(value: number | string | null | undefined): string {
const n = Math.round(Math.abs(Number(value ?? 0)))
+43
View File
@@ -0,0 +1,43 @@
import type { Order } from '@/model/order'
import { usePrice } from '@core/utils/usePrice'
import { getMainMetal } from '@/utils/metalLabels'
export type OrderServiceFeeDisplay = {
value: string
/** از ریال / از مس / از طلا … */
source: string
}
/**
* نمایش کارمزد سفارش با منبع کسر (fee_asset).
* gold → از کیف فلز (گرم + معادل ریالی)
* rial / خالی → از ریال
*/
export function formatOrderServiceFee(order?: Pick<
Order,
'fee_asset' | 'service_charge' | 'service_charge_price' | 'order_metal_label' | 'amount_unit'
> | null): OrderServiceFeeDisplay {
const grams = Number(order?.service_charge ?? 0)
const rial = Number(order?.service_charge_price ?? 0)
if ((!Number.isFinite(grams) || grams <= 0) && (!Number.isFinite(rial) || rial <= 0)) {
return { value: '۰', source: '' }
}
const fromMetal = String(order?.fee_asset ?? '').toLowerCase() === 'gold'
if (fromMetal) {
const metalName = (order?.order_metal_label || getMainMetal() || 'فلز').trim()
const unit = (order?.amount_unit || 'گرم').trim() || 'گرم'
const gramText = Number(grams || 0).toLocaleString('fa-IR', { maximumFractionDigits: 4 })
const rialText = usePrice(rial || 0, true, 'ریال')
return {
value: `${gramText} ${unit} ≈ ${rialText}`,
source: `از ${metalName}`,
}
}
return {
value: usePrice(rial || 0, true, 'ریال'),
source: 'از ریال',
}
}