change transition structure 3

This commit is contained in:
2026-07-11 00:24:30 +03:30
parent 69358a6ff2
commit 4bc9476d59
2 changed files with 279 additions and 122 deletions
@@ -1,123 +1,169 @@
<template>
<section class="dashboard-panel dashboard-profit-split">
<div class="mb-2 flex items-start justify-between gap-2">
<div class="mb-2 flex flex-wrap items-start justify-between gap-2">
<DashboardSectionHeader
class="!mb-0 min-w-0 flex-1"
title="سود طلایی و ریالی"
help="کارمزد معاملات بر اساس فیلدی که مشتری وارد کرده تفکیک می‌شود: وقتی مشتری مبلغ ریالی ثابت می‌زند کارمزد از طلا کسر می‌شود (سود طلایی)، و وقتی وزن ثابت می‌زند کارمزد از ریال کسر می‌شود (سود ریالی)."
:help="sectionHelp"
/>
<a-button type="primary" size="small" @click="openSanad">
ثبت سند تبدیل سود به طلا
</a-button>
</div>
<!-- دو کارت خلاصه -->
<div class="dashboard-profit-split__cards">
<article class="dashboard-profit-split__card dashboard-profit-split__card--gold">
<span class="dashboard-profit-split__card-title">سود طلایی</span>
<p class="dashboard-profit-split__card-hint">کارمزد کسرشده از طلا در این بازه</p>
<p class="dashboard-profit-split__value">
<span class="dashboard-profit-split__amount">{{ formatGram(profitGoldGrams) }}</span>
<span class="dashboard-profit-split__unit">گرم</span>
</p>
<p class="dashboard-profit-split__sub">
≈ {{ formatDashboardAmount(profitGoldValueRial) }} ریال
</p>
</article>
<article class="dashboard-profit-split__card dashboard-profit-split__card--rial">
<span class="dashboard-profit-split__card-title">سود ریالی</span>
<p class="dashboard-profit-split__card-hint">کارمزد کسرشده از ریال در این بازه</p>
<p class="dashboard-profit-split__value">
<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>
</article>
<!-- انتخاب بازه (مستقل از بازهٔ کلی داشبورد، پیش‌فرض ۱۰ روز) -->
<div class="dashboard-profit-split__range mb-3">
<BaseDatePicker
v-model:value="rangeStart"
format="YYYY-MM-DD HH:mm:ss"
class="!w-44"
size="small"
/>
<span class="dashboard-text-muted text-xs">تا</span>
<BaseDatePicker
v-model:value="rangeEnd"
format="YYYY-MM-DD HH:mm:ss"
class="!w-44"
size="small"
/>
<a-button type="primary" size="small" :loading="loading" @click="applyRange">اعمال</a-button>
<a-button size="small" :loading="loading" @click="resetRange">۱۰ روز اخیر</a-button>
<span class="dashboard-text-muted text-[11px]">بازهٔ فعال: {{ activeRangeLabel }}</span>
</div>
<!-- جدول روزانه -->
<div class="mt-3 overflow-x-auto">
<table class="dashboard-table w-full min-w-[34rem] text-sm">
<thead>
<tr class="border-b text-xs">
<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>
</thead>
<tbody>
<tr v-if="!dailyRows.length">
<td colspan="5" class="dashboard-text-muted px-2 py-4 text-center text-xs">
در این بازه معامله‌ای ثبت نشده
</td>
</tr>
<tr
v-for="row in dailyRows"
:key="row.date"
class="border-b hover:bg-[var(--admin-bg-hover)]"
>
<td class="whitespace-nowrap px-2 py-2 text-xs">{{ jalaliDate(row.date) }}</td>
<td class="px-2 py-2 font-semibold" :class="row.profit_gold > 0 ? 'dashboard-profit' : ''">
{{ formatGram(row.profit_gold) }}
</td>
<td class="px-2 py-2 font-semibold" :class="row.profit_rial > 0 ? 'dashboard-profit' : ''">
{{ formatDashboardAmount(row.profit_rial) }}
</td>
<td class="px-2 py-2">{{ formatDashboardAmount(row.spread_rial) }}</td>
<td class="px-2 py-2">{{ Number(row.order_count).toLocaleString('fa-IR') }}</td>
</tr>
</tbody>
</table>
</div>
<a-spin :spinning="loading">
<!-- دو کارت خلاصه -->
<div class="dashboard-profit-split__cards">
<article class="dashboard-profit-split__card dashboard-profit-split__card--gold">
<DashboardLabelWithHelp
label="سود طلایی"
:help="goldHelp"
size="sm"
label-class="dashboard-profit-split__card-title"
/>
<p class="dashboard-profit-split__card-hint">کارمزد کسرشده از طلا در بازهٔ انتخابی</p>
<p class="dashboard-profit-split__value">
<span class="dashboard-profit-split__amount">{{ formatGram(profitGoldGrams) }}</span>
<span class="dashboard-profit-split__unit">گرم</span>
</p>
<p class="dashboard-profit-split__sub">≈ {{ formatDashboardAmount(profitGoldValueRial) }} ریال</p>
</article>
<article class="dashboard-profit-split__card dashboard-profit-split__card--rial">
<DashboardLabelWithHelp
label="سود ریالی"
:help="rialHelp"
size="sm"
label-class="dashboard-profit-split__card-title"
/>
<p class="dashboard-profit-split__card-hint">کارمزد کسرشده از ریال در بازهٔ انتخابی</p>
<p class="dashboard-profit-split__value">
<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>
</article>
</div>
<!-- جدول روزانه -->
<div class="mt-3 overflow-x-auto">
<table class="dashboard-table w-full min-w-[34rem] text-sm">
<thead>
<tr class="border-b text-xs">
<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>
</thead>
<tbody>
<tr v-if="!dailyRows.length">
<td colspan="5" class="dashboard-text-muted px-2 py-4 text-center text-xs">
در این بازه معامله‌ای ثبت نشده
</td>
</tr>
<tr
v-for="row in pagedDaily"
:key="row.date"
class="border-b hover:bg-[var(--admin-bg-hover)]"
>
<td class="whitespace-nowrap px-2 py-2 text-xs">{{ jalaliDate(row.date) }}</td>
<td class="px-2 py-2 font-semibold" :class="row.profit_gold > 0 ? 'dashboard-profit' : ''">
{{ formatGram(row.profit_gold) }}
</td>
<td class="px-2 py-2 font-semibold" :class="row.profit_rial > 0 ? 'dashboard-profit' : ''">
{{ formatDashboardAmount(row.profit_rial) }}
</td>
<td class="px-2 py-2">{{ formatDashboardAmount(row.spread_rial) }}</td>
<td class="px-2 py-2">{{ Number(row.order_count).toLocaleString('fa-IR') }}</td>
</tr>
</tbody>
</table>
</div>
<div v-if="dailyRows.length > dailyPageSize" class="mt-2 flex justify-center">
<a-pagination
v-model:current="dailyPage"
:total="dailyRows.length"
:page-size="dailyPageSize"
:show-size-changer="false"
size="small"
/>
</div>
</a-spin>
<!-- سندهای تبدیل سود به طلا -->
<div class="dashboard-profit-split__sanad mt-3">
<div class="mb-2 flex flex-wrap items-center justify-between gap-2">
<span class="text-sm font-bold">سندهای تبدیل سود ریالی به طلا</span>
<span class="text-sm font-bold dashboard-text">سندهای تبدیل سود ریالی به طلا</span>
<span v-if="conversions.length" class="dashboard-text-muted text-xs">
جمع تبدیل‌شده: {{ formatDashboardAmount(conversionsTotalRial) }} ریال →
جمع تبدیل‌شده در بازه: {{ formatDashboardAmount(conversionsTotalRial) }} ریال →
{{ formatGram(conversionsTotalGram) }} گرم
</span>
</div>
<a-spin :spinning="conversionsLoading">
<div v-if="!conversions.length" class="dashboard-empty py-3 text-center text-xs">
هنوز سندی ثبت نشده
در این بازه سندی ثبت نشده
</div>
<ul v-else class="dashboard-profit-split__sanad-list">
<li
v-for="item in conversions"
:key="item.id"
class="dashboard-profit-split__sanad-item"
>
<div class="min-w-0">
<div class="flex flex-wrap items-center gap-x-2 gap-y-0.5 text-sm">
<span class="font-bold">{{ formatDashboardAmount(item.rial_amount) }} ریال</span>
<span class="dashboard-text-muted">←</span>
<span class="font-bold dashboard-profit">{{ formatGram(item.gram_amount) }} گرم</span>
<span class="dashboard-text-muted text-xs">
(مظنه {{ formatDashboardAmount(item.unit_price) }})
</span>
</div>
<div class="dashboard-text-muted mt-0.5 text-[11px]">
{{ jalaliDate(item.converted_at) }}
<template v-if="item.description">— {{ item.description }}</template>
</div>
</div>
<a-popconfirm
title="این سند حذف شود؟"
ok-text="حذف"
cancel-text="انصراف"
@confirm="removeConversion(item.id)"
<template v-else>
<ul class="dashboard-profit-split__sanad-list">
<li
v-for="item in pagedConversions"
:key="item.id"
class="dashboard-profit-split__sanad-item"
>
<a-button type="text" danger size="small">حذف</a-button>
</a-popconfirm>
</li>
</ul>
<div class="min-w-0">
<div class="flex flex-wrap items-center gap-x-2 gap-y-0.5 text-sm">
<span class="font-bold dashboard-text">{{ formatDashboardAmount(item.rial_amount) }} ریال</span>
<span class="dashboard-text-muted">←</span>
<span class="font-bold dashboard-profit">{{ formatGram(item.gram_amount) }} گرم</span>
<span class="dashboard-text-muted text-xs">(مظنه {{ formatDashboardAmount(item.unit_price) }})</span>
</div>
<div class="dashboard-text-muted mt-0.5 text-[11px]">
{{ jalaliDate(item.converted_at) }}
<template v-if="item.description">— {{ item.description }}</template>
</div>
</div>
<a-popconfirm
title="این سند حذف شود؟"
ok-text="حذف"
cancel-text="انصراف"
@confirm="removeConversion(item.id)"
>
<a-button type="text" danger size="small">حذف</a-button>
</a-popconfirm>
</li>
</ul>
<div v-if="conversions.length > conversionsPageSize" class="mt-2 flex justify-center">
<a-pagination
v-model:current="conversionsPage"
:total="conversions.length"
:page-size="conversionsPageSize"
:show-size-changer="false"
size="small"
/>
</div>
</template>
</a-spin>
</div>
@@ -197,33 +243,82 @@ import { computed, ref, watch } from 'vue'
import moment from 'moment-jalaali'
import { message } from 'ant-design-vue'
import DashboardSectionHeader from '@/pages/dashboard/components/DashboardSectionHeader.vue'
import DashboardLabelWithHelp from '@/pages/dashboard/components/DashboardLabelWithHelp.vue'
import BasicActionSheet from '@/components/BasicActionSheet.vue'
import { formatDashboardAmount } from '@/pages/dashboard/dashboardFormat'
import {
deleteProfitConversion,
getDashboardFinance,
getProfitConversions,
storeProfitConversion,
} from '@/api/dashboard'
import type { DashboardFinance, ProfitConversion } from '@/model/dashboard'
import type { DashboardHelp } from '@/pages/dashboard/dashboardSectionCopy'
import { toJalali } from '@core/utils/useDataTime'
const props = defineProps<{
finance?: DashboardFinance | null
startDate?: string
endDate?: string
/** قیمت هر گرم برای مقدار پیش‌فرض سند */
unitPrice?: number
}>()
const profitGoldGrams = computed(() => Number(props.finance?.profitGoldGrams ?? 0))
const profitRial = computed(() => Number(props.finance?.profitRial ?? 0))
const profitGoldValueRial = computed(() => Number(props.finance?.profitGoldValueRial ?? 0))
const dailyRows = computed(() => props.finance?.daily ?? [])
const DEFAULT_RANGE_DAYS = 10
const DATE_FMT = 'YYYY-MM-DD HH:mm:ss'
function defaultStart() {
return moment().subtract(DEFAULT_RANGE_DAYS, 'days').startOf('day').format(DATE_FMT)
}
function defaultEnd() {
return moment().endOf('day').format(DATE_FMT)
}
const rangeStart = ref(defaultStart())
const rangeEnd = ref(defaultEnd())
// بازه‌ای که هم‌اکنون داده بر اساس آن بارگذاری شده
const activeStart = ref(rangeStart.value)
const activeEnd = ref(rangeEnd.value)
const sectionHelp: DashboardHelp = {
description:
'کارمزد معاملات بر اساس فیلدی که مشتری وارد می‌کند تفکیک می‌شود: وقتی مشتری «مبلغ ریالی» ثابت می‌زند کارمزد از طلا کسر می‌شود (سود طلایی)، و وقتی «وزن» ثابت می‌زند کارمزد از ریال کسر می‌شود (سود ریالی). اعداد بر اساس بازهٔ انتخابی همین بخش محاسبه می‌شوند.',
formula: 'سود طلایی = جمع کارمزدِ کسرشده از طلا (گرم) | سود ریالی = جمع کارمزدِ کسرشده از ریال (ریال)',
}
const goldHelp: DashboardHelp = {
description: 'مجموع کارمزدهایی که در معامله‌های «مبلغ‌محور» (مشتری مبلغ ریالی ثابت زده) از طلا کسر شده است.',
formula: 'مجموع کارمزد طلا (گرم) × قیمت روز = ارزش ریالی تقریبی',
}
const rialHelp: DashboardHelp = {
description: 'مجموع کارمزدهایی که در معامله‌های «وزن‌محور» (مشتری وزن ثابت زده) از ریال کسر شده است.',
formula: 'مجموع کارمزد ریالی معامله‌های وزن‌محور',
}
const loading = ref(false)
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 dailyRows = computed(() => finance.value?.daily ?? [])
// صفحه‌بندی جدول روزانه
const dailyPage = ref(1)
const dailyPageSize = 10
const pagedDaily = computed(() =>
dailyRows.value.slice((dailyPage.value - 1) * dailyPageSize, dailyPage.value * dailyPageSize),
)
// سندها
const conversions = ref<ProfitConversion[]>([])
const conversionsLoading = ref(false)
const conversionsTotalRial = ref(0)
const conversionsTotalGram = ref(0)
const conversionsPage = ref(1)
const conversionsPageSize = 6
const pagedConversions = computed(() =>
conversions.value.slice(
(conversionsPage.value - 1) * conversionsPageSize,
conversionsPage.value * conversionsPageSize,
),
)
const sanadVisible = ref(false)
const submitting = ref(false)
@@ -241,13 +336,17 @@ function emptyForm(): SanadForm {
rial_amount: null,
gram_amount: null,
unit_price: props.unitPrice ? Number(props.unitPrice) : null,
converted_at: moment().format('YYYY-MM-DD HH:mm:ss'),
converted_at: moment().format(DATE_FMT),
description: null,
}
}
const form = ref<SanadForm>(emptyForm())
const activeRangeLabel = computed(
() => `${jalaliDate(activeStart.value)} تا ${jalaliDate(activeEnd.value)}`,
)
function formatGram(value?: number | null) {
return Number(value ?? 0).toLocaleString('fa-IR', { maximumFractionDigits: 4 })
}
@@ -281,13 +380,26 @@ function syncFromGram() {
}
}
async function loadFinance() {
loading.value = true
try {
finance.value = await getDashboardFinance(activeStart.value, activeEnd.value)
dailyPage.value = 1
} catch (e) {
console.error(e)
} finally {
loading.value = false
}
}
async function loadConversions() {
conversionsLoading.value = true
try {
const res = await getProfitConversions(props.startDate, props.endDate)
const res = await getProfitConversions(activeStart.value, activeEnd.value)
conversions.value = res?.items ?? []
conversionsTotalRial.value = Number(res?.totalRial ?? 0)
conversionsTotalGram.value = Number(res?.totalGram ?? 0)
conversionsPage.value = 1
} catch (e) {
console.error(e)
} finally {
@@ -295,6 +407,23 @@ async function loadConversions() {
}
}
function reloadAll() {
void loadFinance()
void loadConversions()
}
function applyRange() {
activeStart.value = rangeStart.value
activeEnd.value = rangeEnd.value
reloadAll()
}
function resetRange() {
rangeStart.value = defaultStart()
rangeEnd.value = defaultEnd()
applyRange()
}
function openSanad() {
form.value = emptyForm()
sanadVisible.value = true
@@ -347,13 +476,7 @@ async function removeConversion(id: number) {
}
}
watch(
() => [props.startDate, props.endDate],
() => {
void loadConversions()
},
{ immediate: true },
)
reloadAll()
</script>
<style scoped lang="less">
@@ -361,6 +484,13 @@ watch(
margin-top: 0.5rem;
}
.dashboard-profit-split__range {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.5rem;
}
.dashboard-profit-split__cards {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
@@ -394,7 +524,7 @@ watch(
background: rgba(14, 165, 233, 0.1);
}
.dashboard-profit-split__card-title {
.dashboard-profit-split__card :deep(.dashboard-profit-split__card-title) {
font-size: 0.8125rem;
font-weight: 700;
color: var(--admin-text);
@@ -411,6 +541,7 @@ watch(
direction: ltr;
text-align: right;
unicode-bidi: isolate;
color: var(--admin-text);
}
.dashboard-profit-split__amount {
@@ -458,19 +589,37 @@ watch(
}
.dashboard-profit-split__form {
@apply flex flex-col gap-3 px-4 pb-2 pt-1;
display: flex;
flex-direction: column;
gap: 0.75rem;
padding: 0.25rem 1rem 0.5rem;
}
.dashboard-profit-split__field {
@apply flex flex-col gap-1;
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.dashboard-profit-split__field label {
@apply text-xs font-semibold text-slate-600 dark:text-slate-300;
font-size: 0.75rem;
font-weight: 600;
color: var(--admin-text-secondary);
}
.dashboard-profit-split__form-footer {
@apply sticky bottom-0 grid grid-cols-2 gap-2 border-t border-slate-200 bg-white pt-3 dark:border-slate-600 dark:bg-slate-900;
position: sticky;
bottom: 0;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.5rem;
padding-top: 0.75rem;
border-top: 1px solid var(--admin-border);
background: var(--admin-bg-elevated);
}
.dashboard-text {
color: var(--admin-text);
}
.dashboard-text-muted {
@@ -485,6 +634,19 @@ watch(
color: #059669;
}
.dashboard-table thead tr,
.dashboard-table tbody tr {
border-color: var(--admin-border);
}
.dashboard-table thead tr {
color: var(--admin-text-secondary);
}
.dashboard-table tbody tr {
color: var(--admin-text);
}
html.is-dark {
.dashboard-profit {
color: #6ee7b7;
+1 -6
View File
@@ -47,12 +47,7 @@
<DashboardTradingIncomeSummary :finance="finance" />
<DashboardProfitSplit
:finance="finance"
:start-date="startDate"
:end-date="endDate"
:unit-price="master?.price"
/>
<DashboardProfitSplit :unit-price="master?.price" />
<!-- تراز و آمار — ۴ کارت در یک ردیف -->
<section class="dashboard-metrics-row mt-2">