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) => export const getAccountTable = async (params: TableInput) =>
await useAxios.post<Array<Card>>('/admin/account/table', params) 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_price?: number | string
/** درصد کارمزد اعمال‌شده */ /** درصد کارمزد اعمال‌شده */
service_charge_percentage?: number | string service_charge_percentage?: number | string
/**
* سمت کسر کارمزد: gold = از کیف فلز/مقداری، rial = از ریال
*/
fee_asset?: 'gold' | 'rial' | string | null
/** مبلغ نقدی جابه‌جاشده: خرید = پرداختی مشتری، فروش = دریافتی مشتری */ /** مبلغ نقدی جابه‌جاشده: خرید = پرداختی مشتری، فروش = دریافتی مشتری */
payment_amount?: number 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"> <div class="flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1">
<DashboardLabelWithHelp <DashboardLabelWithHelp
label="مظنه" label="مظنه"
:help="copy.hero" :help="heroHelp"
size="xs" size="xs"
text-class="text-[11px]" text-class="text-[11px]"
label-class="text-white/80" 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-lg font-bold leading-none sm:text-xl">{{ usePrice(master?.price ?? 0, true, 'ریال') }}</span>
<span class="text-white/35">·</span> <template v-if="showMesghalQuote">
<span class="text-[11px] text-white/80">مثقال</span> <span class="text-white/35">·</span>
<span class="text-sm font-semibold leading-none sm:text-base">{{ usePrice(master?.pricem ?? 0, true, 'ریال') }}</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> </div>
<p v-if="master?.update_date" class="mt-1.5 text-[10px] leading-none text-white/55"> <p v-if="master?.update_date" class="mt-1.5 text-[10px] leading-none text-white/55">
{{ toJalali(master.update_date) }} {{ 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> <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> </tr>
@@ -372,7 +374,15 @@
{{ usePrice(order.total_fee ?? 0, true, 'ریال') }} {{ usePrice(order.total_fee ?? 0, true, 'ریال') }}
</td> </td>
<td class="px-2 py-2.5"> <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>
<td class="px-2 py-2.5"> <td class="px-2 py-2.5">
<span class="rounded-md px-2 py-0.5 text-xs" :class="stsClass(order.sts)"> <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 { useDashboardPage } from '@/pages/dashboard/composables/useDashboardPage'
import { useDashboardQuickActions } from '@/pages/dashboard/composables/useDashboardQuickActions' import { useDashboardQuickActions } from '@/pages/dashboard/composables/useDashboardQuickActions'
import { metalBalanceTitle, metalLabel } from '@/utils/adminMetalLabels' 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 = { const copy = {
hero: dashboardHero, hero: dashboardHero,
@@ -523,6 +536,22 @@ const {
customerStats, customerStats,
} = useDashboardPage() } = 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 tradingProfitTotal = computed(() => {
const priceDiff = Number(finance.value?.fullPriceSood ?? 0) const priceDiff = Number(finance.value?.fullPriceSood ?? 0)
const commission = Number(finance.value?.fullPriceKarmozd ?? finance.value?.fullPriceKaermozd ?? 0) const commission = Number(finance.value?.fullPriceKarmozd ?? finance.value?.fullPriceKaermozd ?? 0)
@@ -17,6 +17,7 @@
:item="item" :item="item"
@open="openSheet" @open="openSheet"
@toggle-sts="confirmToggleSts" @toggle-sts="confirmToggleSts"
@delete="confirmDelete"
/> />
</MobileCardList> </MobileCardList>
@@ -37,6 +38,7 @@ type AccountRow = Card & { sts_str?: string; created_at?: string; customer_id?:
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'toggleSts', item: AccountRow): void (e: 'toggleSts', item: AccountRow): void
(e: 'delete', id: number): void
}>() }>()
const { sheetVisible, selectedItem, openSheet, syncSelectedItem } = useMobileDetailSheet<AccountRow>() 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>({ const { loading, search, items, hasMore, reload, loadMore, fetch } = useMobileTableList<AccountRow>({
fetchApi: getAccountTable, fetchApi: getAccountTable,
onFetched: (rows) => syncSelectedItem(rows) onFetched: (rows) => syncSelectedItem(rows)
@@ -20,6 +20,12 @@
:size="20" :size="20"
@click="$emit('toggleSts', item)" @click="$emit('toggleSts', item)"
/> />
<MobileIconButton
icon="vuesax-linear:trash"
color="#DC2626"
:size="20"
@click="$emit('delete', item)"
/>
</div> </div>
</MobileCard> </MobileCard>
</template> </template>
@@ -38,6 +44,7 @@ const props = defineProps<{
defineEmits<{ defineEmits<{
(e: 'open', item: AccountRow): void (e: 'open', item: AccountRow): void
(e: 'toggleSts', item: AccountRow): void (e: 'toggleSts', item: AccountRow): void
(e: 'delete', item: AccountRow): void
}>() }>()
const customerName = computed(() => props.item.customer?.name || '-') const customerName = computed(() => props.item.customer?.name || '-')
@@ -10,7 +10,7 @@
</BaseButton>--> </BaseButton>-->
</template> </template>
<template #action="{ column, record }"> <template #action="{ column, record }">
<div class="gap-2 flex justify-center"> <div class="gap-2 flex justify-center items-center">
<a-tooltip <a-tooltip
:key="record.sts" :key="record.sts"
placement="bottom" placement="bottom"
@@ -25,6 +25,12 @@
:color="record.sts !== 2 ? 'red' : 'green'" :color="record.sts !== 2 ? 'red' : 'green'"
/> />
</a-tooltip> </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> </div>
</template> </template>
</BaseTable> </BaseTable>
@@ -34,6 +40,7 @@
ref="mobileListRef" ref="mobileListRef"
class="min-h-0 flex-1" class="min-h-0 flex-1"
@toggle-sts="changeAccountStsFunction" @toggle-sts="changeAccountStsFunction"
@delete="(id) => emits('delete', id)"
/> />
</div> </div>
</BasicShowContext> </BasicShowContext>
+3 -202
View File
@@ -2,220 +2,21 @@
<div> <div>
<BasicMainCard> <BasicMainCard>
<div class="flex w-full h-full"> <div class="flex w-full h-full">
<tbl <tbl ref="table" class="w-full h-full" @delete="deleteItem" />
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>-->
</div> </div>
</BasicMainCard> </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> </div>
</template> </template>
<script setup lang="ts"> <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 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 { confirmDeleteItem } from '@/utils/confirmModal'
import { cloneDeep } from 'lodash' import { deleteAccount } from '@/api/card'
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'
//loading
const loading = ref<boolean>(false)
//define store
const store = useStaticApiListsStore()
//define tbl
const table = ref(null) const table = ref(null)
const refreshTable = () => table?.value?.fetch() const refreshTable = () => table?.value?.fetch()
let updateKey = ref<number>(0) const deleteItem = (id: number) => confirmDeleteItem('کارت بانکی', id, deleteAccount, refreshTable)
//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
}
})
}
</script> </script>
<style scoped></style> <style scoped></style>
@@ -31,10 +31,6 @@
<span class="order-detail__field-label">محصول</span> <span class="order-detail__field-label">محصول</span>
<strong>{{ productTitle || '—' }}</strong> <strong>{{ productTitle || '—' }}</strong>
<span class="order-detail__chip">{{ order.order_metal_label || '—' }}</span> <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> </p>
</div> </div>
</header> </header>
@@ -53,13 +49,17 @@
<span class="order-detail__stat-label">جمع سود</span> <span class="order-detail__stat-label">جمع سود</span>
<strong class="order-detail__stat-value">{{ formatOrderRial(grandTotal) }}</strong> <strong class="order-detail__stat-value">{{ formatOrderRial(grandTotal) }}</strong>
<span class="order-detail__stat-unit">ریال</span> <span class="order-detail__stat-unit">ریال</span>
<span class="order-detail__stat-hint">اختلاف قیمت + کارمزد</span>
</article> </article>
</section> </section>
<section class="order-detail__receipt"> <section class="order-detail__receipt">
<div class="order-detail__receipt-head"> <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 class="order-detail__receipt-eq m-0">{{ equationHint }}</p>
<p v-if="feeSourceLabel" class="order-detail__receipt-source m-0">
منبع اختلاف: {{ feeSourceLabel }}
</p>
</div> </div>
<div class="order-detail__receipt-lines"> <div class="order-detail__receipt-lines">
@@ -69,7 +69,7 @@
<div class="min-w-0 flex-1"> <div class="min-w-0 flex-1">
<strong class="order-detail__receipt-label">بدون سود</strong> <strong class="order-detail__receipt-label">بدون سود</strong>
<p class="order-detail__receipt-detail m-0"> <p class="order-detail__receipt-detail m-0">
{{ formatOrderRial(order.price_original ?? 0) }} ریال / گرم × {{ amountLabel }} {{ formatOrderRial(order.price_original ?? 0) }} {{ perUnitLabel }} × {{ amountLabel }}
</p> </p>
</div> </div>
<strong class="order-detail__receipt-amount"> <strong class="order-detail__receipt-amount">
@@ -79,16 +79,16 @@
</div> </div>
</div> </div>
<template v-if="receiptLines.length"> <template v-if="spreadLines.length">
<div <div
v-for="(line, idx) in receiptLines" v-for="(line, idx) in spreadLines"
:key="`${line.key ?? line.kind}-${idx}`" :key="`${line.key ?? line.kind}-${idx}`"
class="order-detail__receipt-line" class="order-detail__receipt-line"
> >
<div class="order-detail__receipt-line-main"> <div class="order-detail__receipt-line-main">
<span class="order-detail__receipt-op" aria-hidden="true">{{ profitSign }}</span> <span class="order-detail__receipt-op" aria-hidden="true">{{ profitSign }}</span>
<div class="min-w-0 flex-1"> <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> <p class="order-detail__receipt-detail m-0">{{ line.line_text || line.detail || '—' }}</p>
</div> </div>
<strong class="order-detail__receipt-amount"> <strong class="order-detail__receipt-amount">
@@ -99,7 +99,7 @@
</div> </div>
</template> </template>
<p v-else class="order-detail__receipt-empty m-0"> <p v-else class="order-detail__receipt-empty m-0">
اقلام سود برای این سفارش ذخیره نشده — سفارش جدید بعد از آپدیت بزنید. سود اختلاف قیمت برای این سفارش ثبت نشده است.
</p> </p>
<div class="order-detail__receipt-line order-detail__receipt-line--result"> <div class="order-detail__receipt-line order-detail__receipt-line--result">
@@ -108,7 +108,7 @@
<div class="min-w-0 flex-1"> <div class="min-w-0 flex-1">
<strong class="order-detail__receipt-label">قیمت مشتری</strong> <strong class="order-detail__receipt-label">قیمت مشتری</strong>
<p class="order-detail__receipt-detail m-0"> <p class="order-detail__receipt-detail m-0">
{{ formatOrderRial(order.price ?? 0) }} ریال / گرم × {{ amountLabel }} {{ formatOrderRial(order.price ?? 0) }} {{ perUnitLabel }} × {{ amountLabel }}
</p> </p>
</div> </div>
<strong class="order-detail__receipt-amount order-detail__receipt-amount--focus"> <strong class="order-detail__receipt-amount order-detail__receipt-amount--focus">
@@ -120,6 +120,48 @@
</div> </div>
</section> </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"> <section class="order-detail__customer">
<div class="order-detail__customer-grid"> <div class="order-detail__customer-grid">
<div class="order-detail__customer-cell"> <div class="order-detail__customer-cell">
@@ -156,10 +198,11 @@ import { formatAmountWithUnit } from '@/utils/weightUnit'
import { import {
formatOrderRial, formatOrderRial,
orderFeeBaseTotal, orderFeeBaseTotal,
orderFeeBreakdown,
orderFeeBreakdownLines,
orderFeeCustomerTotal, orderFeeCustomerTotal,
orderFeeGrandTotal, orderFeeGrandTotal,
orderPricePerUnitLabel,
orderServiceChargeDisplay,
orderSpreadProfitLines,
resolveOrderFeeSourceLabel, resolveOrderFeeSourceLabel,
resolveOrderProductTitle resolveOrderProductTitle
} from '../utils/orderDetailDisplay' } from '../utils/orderDetailDisplay'
@@ -216,18 +259,22 @@ const amountLabel = computed(() => {
const productTitle = computed(() => (order.value ? resolveOrderProductTitle(order.value) : '')) const productTitle = computed(() => (order.value ? resolveOrderProductTitle(order.value) : ''))
const feeSourceLabel = computed(() => (order.value ? resolveOrderFeeSourceLabel(order.value) : '')) const feeSourceLabel = computed(() => (order.value ? resolveOrderFeeSourceLabel(order.value) : ''))
const breakdown = computed(() => (order.value ? orderFeeBreakdown(order.value) : null)) const spreadLines = computed(() => (order.value ? orderSpreadProfitLines(order.value) : []))
const usesAppPrice = computed(() => Boolean(breakdown.value?.uses_app_price)) const serviceFee = computed(() =>
const receiptLines = computed(() => (order.value ? orderFeeBreakdownLines(order.value) : [])) order.value
? orderServiceChargeDisplay(order.value)
: { total: 0, label: 'کارمزد معامله', detail: '—', source: '', value: '۰' }
)
const grandTotal = computed(() => (order.value ? orderFeeGrandTotal(order.value) : 0)) const grandTotal = computed(() => (order.value ? orderFeeGrandTotal(order.value) : 0))
const baseTotal = computed(() => (order.value ? orderFeeBaseTotal(order.value) : 0)) const baseTotal = computed(() => (order.value ? orderFeeBaseTotal(order.value) : 0))
const customerTotal = computed(() => (order.value ? orderFeeCustomerTotal(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 isSell = computed(() => order.value?.direction === 2)
const profitSign = computed(() => (isSell.value ? '−' : '+')) const profitSign = computed(() => (isSell.value ? '−' : '+'))
const equationHint = computed(() => const equationHint = computed(() =>
isSell.value isSell.value
? 'بدون سود − سود = قیمت مشتری' ? 'بدون سود − سود اختلاف قیمت = قیمت مشتری'
: 'بدون سود + سود = قیمت مشتری' : 'بدون سود + سود اختلاف قیمت = قیمت مشتری'
) )
function close() { function close() {
@@ -331,6 +378,11 @@ function close() {
color: var(--admin-text-secondary); color: var(--admin-text-secondary);
} }
.order-detail__stat-hint {
@apply mt-0.5 block text-[11px];
color: var(--admin-text-secondary);
}
.order-detail__chip { .order-detail__chip {
@apply rounded-lg px-2 py-0.5 text-xs font-600; @apply rounded-lg px-2 py-0.5 text-xs font-600;
border: 1px solid var(--admin-border); border: 1px solid var(--admin-border);
@@ -340,7 +392,12 @@ function close() {
.order-detail__chip--soft { .order-detail__chip--soft {
color: var(--admin-text-secondary); color: var(--admin-text-secondary);
font-weight: 500; 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 { .order-detail__receipt {
@@ -363,6 +420,10 @@ function close() {
color: var(--admin-text-secondary); color: var(--admin-text-secondary);
} }
.order-detail__receipt-source {
@apply mt-1 text-xs text-primary;
}
.order-detail__receipt-lines { .order-detail__receipt-lines {
@apply flex flex-col gap-2; @apply flex flex-col gap-2;
} }
@@ -45,31 +45,45 @@
/> />
</div> </div>
<div v-if="orderFeeBreakdownLines(row).length" class="space-y-2 rounded-xl border border-[var(--admin-border)] p-3"> <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-sm font-bold">سود اختلاف قیمت</p>
<div <p v-if="feeSourceLabel(row)" class="m-0 text-xs opacity-70">منبع: {{ feeSourceLabel(row) }}</p>
v-for="(line, idx) in orderFeeBreakdownLines(row)" <template v-if="spreadLines(row).length">
:key="`${line.key ?? line.kind}-${idx}`" <div
class="text-xs" v-for="(line, idx) in spreadLines(row)"
> :key="`${line.key ?? line.kind}-${idx}`"
<p class="m-0 font-600">+ {{ line.label }}</p> class="text-xs"
<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> <p class="m-0 font-600">{{ line.label }}</p>
</div> <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> </div>
<MobileInfoGrid> <MobileInfoGrid>
<MobileDetailRow label="محصول" :value="productTitle(row) || '-'" /> <MobileDetailRow label="محصول" :value="productTitle(row) || '-'" />
<MobileDetailRow label="نوع فلز" :value="row.order_metal_label ?? '-'" /> <MobileDetailRow label="نوع فلز" :value="row.order_metal_label ?? '-'" />
<MobileDetailRow label="منبع اختلاف قیمت" :value="feeSourceLabel(row) || '-'" />
<MobileDetailRow label="کد سفارش" :value="row.id ?? '-'" /> <MobileDetailRow label="کد سفارش" :value="row.id ?? '-'" />
<MobileDetailRow label="شماره موبایل" :value="row.customer?.mobile || '-'" /> <MobileDetailRow label="شماره موبایل" :value="row.customer?.mobile || '-'" />
<MobileDetailRow <MobileDetailRow
label="سود اختلاف قیمت (ریال)" label="سود اختلاف قیمت (ریال)"
:value="usePrice(orderProfitTotal(row), true, '')" :value="usePrice(spreadTotal(row), true, '')"
/> />
<MobileDetailRow label="کارمزد (طلا)" :value="serviceChargeLabel(row)" /> <MobileDetailRow label="جمع سود (اختلاف + کارمزد)" :value="usePrice(orderFeeGrandTotal(row), true, '')" />
<MobileDetailRow label="کارمزد (ریال)" :value="usePrice(row.service_charge_price ?? 0, true, '')" />
<MobileDetailRow label="کد کاربری" :value="row.customer?.id ?? '-'" /> <MobileDetailRow label="کد کاربری" :value="row.customer?.id ?? '-'" />
<MobileDetailRow label="شماره ملی" :value="row.customer?.n_code || '-'" /> <MobileDetailRow label="شماره ملی" :value="row.customer?.n_code || '-'" />
<MobileDetailRow label="شماره تلفن" :value="row.customer?.phone_number || '-'" /> <MobileDetailRow label="شماره تلفن" :value="row.customer?.phone_number || '-'" />
@@ -91,10 +105,10 @@ import { isMobile } from '@core/utils/is'
import { toJalali } from '@core/utils/useDataTime' import { toJalali } from '@core/utils/useDataTime'
import { usePrice } from '@core/utils/usePrice' import { usePrice } from '@core/utils/usePrice'
import { formatAmountWithUnit } from '@/utils/weightUnit' import { formatAmountWithUnit } from '@/utils/weightUnit'
import { formatOrderServiceFee } from '@/utils/orderServiceFee'
import { import {
orderFeeBreakdownLines,
orderFeeGrandTotal, orderFeeGrandTotal,
orderProfitTotal, orderSpreadProfitLines,
resolveOrderFeeSourceLabel, resolveOrderFeeSourceLabel,
resolveOrderProductTitle resolveOrderProductTitle
} from '../utils/orderDetailDisplay' } from '../utils/orderDetailDisplay'
@@ -159,11 +173,16 @@ function amountLabel(row: OrderRow) {
return formatAmountWithUnit(row.amount_display ?? row.amount, row.amount_unit) return formatAmountWithUnit(row.amount_display ?? row.amount, row.amount_unit)
} }
function serviceChargeLabel(row: OrderRow) { function serviceFeeDisplay(row: OrderRow) {
if (row.service_charge == null || row.service_charge === '') return '-' return formatOrderServiceFee(row)
const value = Number(row.service_charge) }
if (Number.isNaN(value)) return String(row.service_charge)
return formatAmountWithUnit(value, row.amount_unit) 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) { function productTitle(row: OrderRow) {
@@ -3,6 +3,7 @@ import { toJalali } from '@core/utils/useDataTime'
import CustomTag from '@/components/CustomTag.vue' import CustomTag from '@/components/CustomTag.vue'
import { usePrice } from '@core/utils/usePrice' import { usePrice } from '@core/utils/usePrice'
import { formatAmountWithUnit } from '@/utils/weightUnit' import { formatAmountWithUnit } from '@/utils/weightUnit'
import { formatOrderServiceFee } from '@/utils/orderServiceFee'
export type OrderMetalColumn = { metal_id: number; label: string; display_unit?: string } export type OrderMetalColumn = { metal_id: number; label: string; display_unit?: string }
@@ -103,13 +104,18 @@ export function buildOrderColumns(_metals: OrderMetalColumn[] = []): BaseTableCo
title: 'کارمزد', title: 'کارمزد',
dataIndex: 'service_charge', dataIndex: 'service_charge',
key: 'service_charge', key: 'service_charge',
type: 'Number', type: 'Text',
customRender({ record }) { customRender({ record }) {
const value = record.service_charge const fee = formatOrderServiceFee(record)
if (value == null || value === '') { if (!fee.source) {
return '-' 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 type { Order, OrderFeeBreakdown, OrderFeeBreakdownLine } from '@/model/order'
import { formatOrderServiceFee } from '@/utils/orderServiceFee'
import { formatAmountWithUnit } from '@/utils/weightUnit'
type TxLike = { type TxLike = {
title?: string | null title?: string | null
@@ -6,6 +8,15 @@ type TxLike = {
product?: { id?: number; title?: string | null } | null 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 { export function resolveOrderProductTitle(order: Order & { transactions?: TxLike[] }): string {
const direct = (order.product_title ?? '').trim() const direct = (order.product_title ?? '').trim()
if (direct) return direct if (direct) return direct
@@ -72,32 +83,126 @@ export function orderFeeBreakdownLines(order: Order): OrderFeeBreakdownLine[] {
return orderFeeBreakdown(order)?.lines ?? [] 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 { export function orderFeeGrandTotal(order: Order): number {
const breakdown = orderFeeBreakdown(order) const breakdown = orderFeeBreakdown(order)
if (breakdown && breakdown.grand_total != null) { 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)) const service = Math.abs(Number(order.service_charge_price ?? 0))
return Math.round(spread + service) 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 { 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) const breakdown = orderFeeBreakdown(order)
if (breakdown?.base_total != null) { 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 { export function orderFeeCustomerTotal(order: Order): number {
const breakdown = orderFeeBreakdown(order) const breakdown = orderFeeBreakdown(order)
if (breakdown?.customer_total != null) { 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))) 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 ویرگول اشتباه نگذارد */ /** مبالغ ریالی — بدون اعشار تا usePrice ویرگول اشتباه نگذارد */
export function formatOrderRial(value: number | string | null | undefined): string { export function formatOrderRial(value: number | string | null | undefined): string {
const n = Math.round(Math.abs(Number(value ?? 0))) 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: 'از ریال',
}
}