added charge for all products 2

This commit is contained in:
2026-07-11 22:31:45 +03:30
parent 654002f1d6
commit 6107a2cbf3
13 changed files with 1010 additions and 213 deletions
@@ -55,6 +55,9 @@
<span class="dashboard-profit-split__unit">گرم</span>
</p>
<p class="dashboard-profit-split__sub">≈ {{ formatDashboardAmount(profitGoldValueRial) }} ریال</p>
<p v-if="profitGoldConvertedGram > 0" class="dashboard-profit-split__note">
شامل {{ formatGram(profitGoldConvertedGram) }} گرم از تبدیل سود ریالی
</p>
</article>
<article class="dashboard-profit-split__card dashboard-profit-split__card--rial">
@@ -69,7 +72,14 @@
<span class="dashboard-profit-split__amount">{{ formatDashboardAmount(profitRial) }}</span>
<span class="dashboard-profit-split__unit">ریال</span>
</p>
<p class="dashboard-profit-split__sub">{{ dailyRows.length }} روز دارای معامله</p>
<p class="dashboard-profit-split__sub">
<template v-if="profitRialConverted > 0">
{{ formatGram(profitGoldConvertedGram) }} گرم تبدیل‌شده به طلا
</template>
<template v-else>
{{ dailyRows.length }} روز دارای معامله
</template>
</p>
</article>
</div>
@@ -291,15 +301,15 @@ const activeEnd = ref(rangeEnd.value)
const sectionHelp: DashboardHelp = {
description:
'کارمزد معاملات بر اساس فیلدی که مشتری وارد می‌کند تفکیک می‌شود: وقتی مشتری «مبلغ ریالی» ثابت می‌زند کارمزد از طلا کسر می‌شود (سود طلایی)، و وقتی «وزن» ثابت می‌زند کارمزد از ریال کسر می‌شود (سود ریالی). اعداد بر اساس بازهٔ انتخابی همین بخش محاسبه می‌شوند.',
formula: 'سود طلایی = جمع کارمزدِ کسرشده از طلا (گرم) | سود ریالی = جمع کارمزدِ کسرشده از ریال (ریال)',
formula: 'سود طلایی = جمع کارمزدِ کسرشده از طلا (گرم) + گرم سندهای تبدیل سود ریالی | سود ریالی = جمع کارمزدِ کسرشده از ریال − ریال تبدیل‌شده به طلا',
}
const goldHelp: DashboardHelp = {
description: 'مجموع کارمزدهایی که در معامله‌های «مبلغ‌محور» (مشتری مبلغ ریالی ثابت زده) از طلا کسر شده است.',
formula: 'مجموع کارمزد طلا (گرم) × قیمت روز = ارزش ریالی تقریبی',
description: 'مجموع کارمزدهای کسرشده از طلا به‌علاوهٔ گرم سندهای «تبدیل سود ریالی به طلا» در همین بازه.',
formula: 'کارمزد طلا (گرم) + گرم تبدیل‌شده از سود ریالی',
}
const rialHelp: DashboardHelp = {
description: 'مجموع کارمزدهایی که در معامله‌های «وزن‌محور» (مشتری وزن ثابت زده) از ریال کسر شده است.',
formula: 'مجموع کارمزد ریالی معامله‌های وزن‌محور',
description: 'سود ریالی باقی‌مانده پس از کسر سندهای تبدیل به طلا در همین بازه.',
formula: 'کارمزد ریالی − ریال تبدیل‌شده به طلا',
}
const loading = ref(false)
@@ -308,6 +318,8 @@ const finance = ref<DashboardFinance | null>(null)
const profitGoldGrams = computed(() => Number(finance.value?.profitGoldGrams ?? 0))
const profitRial = computed(() => Number(finance.value?.profitRial ?? 0))
const profitGoldValueRial = computed(() => Number(finance.value?.profitGoldValueRial ?? 0))
const profitRialConverted = computed(() => Number(finance.value?.profitRialConverted ?? 0))
const profitGoldConvertedGram = computed(() => Number(finance.value?.profitGoldConvertedGram ?? 0))
const dailyRows = computed(() => finance.value?.daily ?? [])
// صفحه‌بندی جدول روزانه
@@ -467,7 +479,7 @@ async function submitSanad() {
})
message.success('سند تبدیل سود ثبت شد')
closeSanad()
await loadConversions()
reloadAll()
} catch (e) {
console.error(e)
message.error('خطا در ثبت سند')
@@ -480,7 +492,7 @@ async function removeConversion(id: number) {
try {
await deleteProfitConversion(id)
message.success('سند حذف شد')
await loadConversions()
reloadAll()
} catch (e) {
console.error(e)
message.error('خطا در حذف سند')
@@ -595,6 +607,12 @@ reloadAll()
color: var(--admin-text-secondary);
}
.dashboard-profit-split__note {
margin: 0.2rem 0 0;
font-size: 0.6875rem;
color: var(--admin-text-secondary);
}
.dashboard-profit-split__sanad {
border-top: 1px solid var(--admin-border);
padding-top: 0.75rem;
@@ -1,78 +1,96 @@
<template>
<MobileDetailSheet v-model:visible="visibleProxy" :item="item" title="">
<MobileDetailSheet v-model:visible="visibleProxy" :item="item" :title="sheetTitle">
<template #default="{ item: row }">
<h2 class="text-base font-700 leading-relaxed text-gray-900">{{ row.name || '-' }}</h2>
<p v-if="row.mobile" class="text-sm text-gray-500" dir="ltr">{{ row.mobile }}</p>
<div class="customer-mobile-sheet">
<p v-if="row.mobile" class="customer-mobile-sheet__mobile" dir="ltr">{{ row.mobile }}</p>
<div class="customer-mobile-sheet__chips">
<span class="customer-mobile-sheet__chip" :class="row.active ? 'is-success' : 'is-danger'">
{{ row.active ? 'فعال' : 'غیرفعال' }}
</span>
<span
class="customer-mobile-sheet__chip"
:class="row.trade_active === 1 ? 'is-success' : 'is-danger'"
>
{{ row.trade_active === 1 ? 'سفارش مجاز' : 'سفارش ممنوع' }}
</span>
</div>
<section class="customer-mobile-sheet__section">
<h4 class="customer-mobile-sheet__section-title">موجودی</h4>
<CustomerWalletBalancePanel
:customer-id="row.id"
:charge-targets="chargeTargets"
:interactive="false"
compact
/>
</section>
<div class="grid grid-cols-2 gap-2">
<BaseButton class="w-full col-span-2" size="middle" type="primary" icon="vuesax-linear:wallet-add" @click="closeAndEmit('chargeWallet', row)">
شارژ کیف پول
</BaseButton>
<BaseButton
v-if="!row.is_legal"
class="w-full"
class="customer-mobile-sheet__charge-btn"
size="middle"
type="default"
icon="vuesax-linear:user-search"
@click="closeAndEmit('confirmUser', row)"
type="primary"
icon="vuesax-linear:wallet-add"
@click="closeAndEmit('chargeWallet', row)"
>
تایید کاربر
</BaseButton>
<BaseButton class="w-full" size="middle" type="default" icon="vuesax-linear:stickynote" @click="closeAndEmit('info', row)">
مشخصات کامل
</BaseButton>
<BaseButton class="w-full" size="middle" type="default" icon="vuesax-linear:edit" @click="closeAndEmit('edit', row)">
ویرایش
</BaseButton>
<BaseButton class="w-full" size="middle" type="default" icon="vuesax-linear:archive-tick" @click="closeAndEmit('archive', row)">
بایگانی
</BaseButton>
<BaseButton
class="w-full"
size="middle"
:type="row.active ? 'default' : 'primary'"
:icon="row.active ? 'vuesax-linear:user-remove' : 'vuesax-linear:user-tick'"
@click="confirmToggleActive(row)"
>
{{ row.active ? 'غیرفعال کردن' : 'فعال کردن' }}
</BaseButton>
<BaseButton
class="w-full"
size="middle"
:type="row.trade_active === 1 ? 'default' : 'primary'"
:icon="row.trade_active === 1 ? 'vuesax-linear:box-remove' : 'vuesax-linear:box-tick'"
@click="confirmToggleTrade(row)"
>
{{ row.trade_active === 1 ? 'ممنوعیت سفارش' : 'مجاز کردن سفارش' }}
شارژ / دشارژ کیف پول
</BaseButton>
<div class="customer-mobile-sheet__actions">
<BaseButton size="small" type="default" icon="vuesax-linear:stickynote" @click="closeAndEmit('info', row)">
مشخصات
</BaseButton>
<BaseButton size="small" type="default" icon="vuesax-linear:edit" @click="closeAndEmit('edit', row)">
ویرایش
</BaseButton>
<BaseButton size="small" type="default" icon="vuesax-linear:archive-tick" @click="closeAndEmit('archive', row)">
بایگانی
</BaseButton>
<BaseButton
v-if="!row.is_legal"
size="small"
type="default"
icon="vuesax-linear:user-search"
@click="closeAndEmit('confirmUser', row)"
>
تایید
</BaseButton>
<BaseButton
size="small"
:type="row.active ? 'default' : 'primary'"
:icon="row.active ? 'vuesax-linear:user-remove' : 'vuesax-linear:user-tick'"
@click="confirmToggleActive(row)"
>
{{ row.active ? 'غیرفعال' : 'فعال' }}
</BaseButton>
<BaseButton
size="small"
:type="row.trade_active === 1 ? 'default' : 'primary'"
:icon="row.trade_active === 1 ? 'vuesax-linear:box-remove' : 'vuesax-linear:box-tick'"
@click="confirmToggleTrade(row)"
>
{{ row.trade_active === 1 ? 'ممنوعیت' : 'مجاز سفارش' }}
</BaseButton>
</div>
<MobileInfoGrid>
<MobileDetailRow label="کد کاربری" :value="row.id ?? '-'" />
<MobileDetailRow label="نوع کاربر" :value="row.type_str || '-'" />
<MobileDetailRow label="کد معرف" :value="row.referral_id ?? '-'" />
<MobileDetailRow label="نام معرف" :value="row.referral?.name ?? '-'" />
<MobileDetailRow label="آخرین بازدید" :value="lastSeen(row)" value-class="text-xs" />
</MobileInfoGrid>
</div>
<CustomerWalletBalancePanel :customer-id="row?.id" />
<MobileInfoGrid>
<MobileDetailRow label="وضعیت کاربر" full-width :value-class="row.active ? 'text-green-700' : 'text-red-600'">
{{ row.active ? 'فعال' : 'غیرفعال' }}
</MobileDetailRow>
<MobileDetailRow
label="وضعیت سفارش دهی"
full-width
:value-class="row.trade_active === 1 ? 'text-green-700' : 'text-red-600'"
>
{{ row.trade_active === 1 ? 'مجاز' : 'غیر مجاز' }}
</MobileDetailRow>
<MobileDetailRow label="کد کاربری" :value="row.id ?? '-'" />
<MobileDetailRow label="نوع کاربر" :value="row.type_str || '-'" />
<MobileDetailRow label="کد معرف" :value="row.referral_id ?? '-'" />
<MobileDetailRow label="نام معرف کاربر" :value="row.referral?.name ?? '-'" />
<MobileDetailRow label="آخرین بازدید" :value="lastSeen(row)" value-class="text-xs" />
</MobileInfoGrid>
</template>
</MobileDetailSheet>
</template>
<script setup lang="ts">
import { computed, ref, watch } from 'vue'
import { Modal } from 'ant-design-vue'
import type { Customer } from '@/model/customer'
import type { ChargeTarget } from '@/model/wallet'
import { getChargeTargets } from '@/api/wallet'
import { MODAL_ABOVE_ACTION_SHEET_Z_INDEX } from '@/constants/overlayZIndex'
import MobileDetailRow from '@/components/mobile/MobileDetailRow.vue'
import MobileDetailSheet from '@/components/mobile/MobileDetailSheet.vue'
@@ -96,11 +114,15 @@ const emit = defineEmits<{
(e: 'confirmUser', item: Customer): void
}>()
const chargeTargets = ref<ChargeTarget[]>([])
const visibleProxy = computed({
get: () => props.visible,
set: (value: boolean) => emit('update:visible', value)
set: (value: boolean) => emit('update:visible', value),
})
const sheetTitle = computed(() => props.item?.name?.trim() || 'مشتری')
type SheetEmit = 'archive' | 'chargeWallet' | 'info' | 'confirmUser' | 'edit'
function closeAndEmit(event: SheetEmit, row: Customer) {
@@ -115,7 +137,7 @@ function confirmToggleActive(row: Customer) {
cancelText: 'رد',
centered: true,
zIndex: MODAL_ABOVE_ACTION_SHEET_Z_INDEX,
onOk: () => emit('toggleActive', row)
onOk: () => emit('toggleActive', row),
})
}
@@ -126,11 +148,98 @@ function confirmToggleTrade(row: Customer) {
cancelText: 'رد',
centered: true,
zIndex: MODAL_ABOVE_ACTION_SHEET_Z_INDEX,
onOk: () => emit('toggleTrade', row)
onOk: () => emit('toggleTrade', row),
})
}
function lastSeen(row: Customer) {
return row.last_seen_at ? toJalali(row.last_seen_at) : '-'
}
async function loadChargeTargets() {
try {
const res = await getChargeTargets()
chargeTargets.value = res?.targets ?? []
} catch {
chargeTargets.value = []
}
}
watch(
() => [props.visible, props.item?.id] as const,
([open]) => {
if (open) void loadChargeTargets()
},
)
</script>
<style scoped>
.customer-mobile-sheet {
display: flex;
flex-direction: column;
gap: 0.75rem;
width: 100%;
min-width: 0;
max-width: 100%;
}
.customer-mobile-sheet__mobile {
margin: 0;
font-size: 0.8125rem;
color: var(--admin-text-secondary);
}
.customer-mobile-sheet__chips {
display: flex;
flex-wrap: wrap;
gap: 0.35rem;
}
.customer-mobile-sheet__chip {
padding: 0.2rem 0.55rem;
border-radius: 9999px;
font-size: 0.6875rem;
font-weight: 700;
}
.customer-mobile-sheet__chip.is-success {
background: rgba(34, 197, 94, 0.14);
color: #15803d;
}
.customer-mobile-sheet__chip.is-danger {
background: rgba(239, 68, 68, 0.14);
color: #b91c1c;
}
.customer-mobile-sheet__section {
width: 100%;
min-width: 0;
max-width: 100%;
}
.customer-mobile-sheet__section-title {
margin: 0 0 0.35rem;
font-size: 0.75rem;
font-weight: 700;
color: var(--admin-text-secondary);
}
.customer-mobile-sheet__charge-btn {
width: 100%;
}
.customer-mobile-sheet__actions {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 0.35rem;
}
:global(html.is-dark) .customer-mobile-sheet__chip.is-success {
color: #4ade80;
}
:global(html.is-dark) .customer-mobile-sheet__chip.is-danger {
color: #f87171;
}
</style>
@@ -138,12 +138,27 @@ defineExpose({
margin-bottom: 0.65rem;
}
.wallet-charge-form :deep(.ant-form-item-label > label) {
color: var(--admin-text, rgba(0, 0, 0, 0.88));
}
.wallet-charge-form__direction :deep(.ant-radio-button-wrapper) {
width: 50%;
text-align: center;
height: 2.35rem;
line-height: 2.25rem;
font-weight: 700;
background: var(--admin-bg-muted, #fafafa) !important;
border-color: var(--admin-border-strong, #d9d9d9) !important;
color: var(--admin-text, rgba(0, 0, 0, 0.88)) !important;
}
.wallet-charge-form__direction :deep(.ant-radio-button-wrapper:not(:first-child)::before) {
background-color: var(--admin-border-strong, #d9d9d9) !important;
}
.wallet-charge-form__direction :deep(.ant-radio-button-wrapper:hover) {
color: var(--admin-text, rgba(0, 0, 0, 0.88)) !important;
}
.wallet-charge-form__radio-icon {
@@ -180,29 +195,29 @@ defineExpose({
.wallet-charge-form__preview--charge {
background: rgba(34, 197, 94, 0.12);
border-color: rgba(34, 197, 94, 0.35);
color: #86efac;
color: #15803d;
}
.wallet-charge-form__preview--charge .wallet-charge-form__preview-sign {
background: rgba(34, 197, 94, 0.2);
color: #4ade80;
color: #16a34a;
}
.wallet-charge-form__preview--discharge {
background: rgba(239, 68, 68, 0.12);
border-color: rgba(239, 68, 68, 0.35);
color: #fca5a5;
color: #b91c1c;
}
.wallet-charge-form__preview--discharge .wallet-charge-form__preview-sign {
background: rgba(239, 68, 68, 0.2);
color: #f87171;
color: #dc2626;
}
.wallet-charge-form__hint {
margin-top: 0.35rem;
font-size: 0.75rem;
opacity: 0.75;
color: var(--admin-text-secondary, rgba(0, 0, 0, 0.45));
}
.wallet-charge-form__direction :deep(.ant-radio-button-wrapper-checked.wallet-charge-form__radio--charge) {
@@ -211,9 +226,59 @@ defineExpose({
color: #fff !important;
}
.wallet-charge-form__direction :deep(.ant-radio-button-wrapper-checked.wallet-charge-form__radio--charge::before) {
background-color: #15803d !important;
}
.wallet-charge-form__direction :deep(.ant-radio-button-wrapper-checked.wallet-charge-form__radio--discharge) {
background: #b91c1c !important;
border-color: #b91c1c !important;
color: #fff !important;
}
.wallet-charge-form__direction :deep(.ant-radio-button-wrapper-checked.wallet-charge-form__radio--discharge::before) {
background-color: #b91c1c !important;
}
:global(html.is-dark) .wallet-charge-form__preview--charge {
background: rgba(34, 197, 94, 0.14);
border-color: rgba(34, 197, 94, 0.32);
color: #86efac;
}
:global(html.is-dark) .wallet-charge-form__preview--charge .wallet-charge-form__preview-sign {
background: rgba(34, 197, 94, 0.22);
color: #4ade80;
}
:global(html.is-dark) .wallet-charge-form__preview--discharge {
background: rgba(239, 68, 68, 0.14);
border-color: rgba(239, 68, 68, 0.32);
color: #fca5a5;
}
:global(html.is-dark) .wallet-charge-form__preview--discharge .wallet-charge-form__preview-sign {
background: rgba(239, 68, 68, 0.22);
color: #f87171;
}
:global(html.is-dark) .wallet-charge-form__direction :deep(.ant-radio-button-wrapper-checked.wallet-charge-form__radio--charge) {
background: #16a34a !important;
border-color: #16a34a !important;
color: #fff !important;
}
:global(html.is-dark) .wallet-charge-form__direction :deep(.ant-radio-button-wrapper-checked.wallet-charge-form__radio--charge::before) {
background-color: #16a34a !important;
}
:global(html.is-dark) .wallet-charge-form__direction :deep(.ant-radio-button-wrapper-checked.wallet-charge-form__radio--discharge) {
background: #dc2626 !important;
border-color: #dc2626 !important;
color: #fff !important;
}
:global(html.is-dark) .wallet-charge-form__direction :deep(.ant-radio-button-wrapper-checked.wallet-charge-form__radio--discharge::before) {
background-color: #dc2626 !important;
}
</style>
@@ -15,14 +15,15 @@
/>
</div>
<CustomerWalletBalancePanel
v-if="resolvedCustomerId && !targetsLoading"
:customer-id="resolvedCustomerId"
:charge-targets="targets"
:selected-charge-key="selectedTargetKey"
compact
@select-charge-target="selectedTargetKeyProxy = $event"
/>
<div v-if="resolvedCustomerId && !targetsLoading" class="wallet-charge-modal-body__assets">
<CustomerWalletBalancePanel
:customer-id="resolvedCustomerId"
:charge-targets="targets"
:selected-charge-key="selectedTargetKey"
compact
@select-charge-target="selectedTargetKeyProxy = $event"
/>
</div>
<div class="wallet-charge-modal-body__section">
<label class="wallet-charge-modal-body__label">محصول / کیف پول</label>
@@ -151,6 +152,13 @@ defineExpose({
padding: 0 0.75rem 0.25rem;
}
.wallet-charge-modal-body__assets {
margin-bottom: 0.75rem;
width: 100%;
min-width: 0;
max-width: 100%;
}
.wallet-charge-modal-body__section {
display: flex;
flex-direction: column;
+39 -4
View File
@@ -132,10 +132,12 @@
:showButton="false"
@close="userInfoVisible = false"
>
<div v-if="isMobile" class="mb-3 text-base font-700">اطلاعات کاربر</div>
<a-tabs v-if="userInfo" v-model:activeKey="activeKey">
<a-tab-pane key="اطلاعات جامع" tab="اطلاعات جامع">
<div class="space-y-2">
<div
class="grid min-w-0 grid-cols-1 items-start gap-3 lg:grid-cols-[minmax(17rem,21rem)_minmax(0,1fr)] xl:grid-cols-[minmax(18rem,23rem)_minmax(0,1fr)]"
>
<div class="min-w-0 space-y-2 lg:order-2">
<div class="admin-stat-muted rounded-lg w-full flex flex-col gap-3 p-3 md:flex-row md:justify-between">
<div>
<div class="font-bold">{{ userInfo.name }}</div>
@@ -172,7 +174,7 @@
</div>
</div>
<div class="grid grid-cols-2 gap-2 md:grid-cols-4">
<div class="grid min-w-0 grid-cols-2 gap-2 md:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5">
<BasicInfoItem title="کد کاربری" :value="userInfo?.id" />
<BasicInfoItem title="نوع کاربر" :value="userInfo?.type_str" />
<BasicInfoItem
@@ -215,7 +217,26 @@
</BasicInfoItem>
</div>
<CustomerWalletBalancePanel v-if="userInfo?.id" :customer-id="userInfo.id" class="mt-2" />
<CustomerWalletBalancePanel
v-if="userInfo?.id && isMobile"
:customer-id="userInfo.id"
compact
:charge-targets="infoChargeTargets"
:interactive="false"
class="mt-2"
/>
</div>
<aside
v-if="userInfo?.id && !isMobile"
class="surface-muted min-w-0 rounded-xl border border-base p-2.5 lg:order-1 lg:sticky lg:top-0"
>
<CustomerWalletBalancePanel
:customer-id="userInfo.id"
layout="sidebar"
:interactive="false"
/>
</aside>
</div>
</a-tab-pane>
<a-tab-pane key="لیست سفارشات" tab="لیست سفارشات">
@@ -283,6 +304,8 @@ import Icon from '@core/components/BaseIcon/src/Icon.vue'
import { isMobile } from '@core/utils/is'
import WalletChargeModal from './components/WalletChargeModal.vue'
import CustomerWalletBalancePanel from '@/components/customer/CustomerWalletBalancePanel.vue'
import { getChargeTargets } from '@/api/wallet'
import type { ChargeTarget } from '@/model/wallet'
import { metalLabel } from '@/utils/adminMetalLabels'
import type { DashboardHelp } from '@/pages/dashboard/dashboardSectionCopy'
import DashboardLabelWithHelp from '@/pages/dashboard/components/DashboardLabelWithHelp.vue'
@@ -482,6 +505,7 @@ async function info(val: Customer) {
activeKey.value = 'اطلاعات جامع'
userInfo.value = cloneDeep(val)
userInfoVisible.value = true
void loadInfoChargeTargets()
await nextTick(() => {
loading.value = true
refreshFundTable()
@@ -551,6 +575,17 @@ onMounted(async () => {
//add project modal
const addUserVisible = ref<boolean>(false)
const userInfoVisible = ref<boolean>(false)
const infoChargeTargets = ref<ChargeTarget[]>([])
async function loadInfoChargeTargets() {
if (!isMobile.value) return
try {
const res = await getChargeTargets()
infoChargeTargets.value = res?.targets ?? []
} catch {
infoChargeTargets.value = []
}
}
const userInfo = ref<Customer>()
//submit user