change transition structure 2

This commit is contained in:
2026-07-10 23:25:40 +03:30
parent 62f615f5c1
commit 69358a6ff2
4 changed files with 566 additions and 0 deletions
+19
View File
@@ -6,6 +6,8 @@ import type {
DashboardMaster,
DashboardOrderPeakHours,
PriceChartPoint,
ProfitConversion,
ProfitConversionsResponse,
} from '@/model/dashboard'
import type { Order } from '@/model/order'
import type { Balance } from '@/model/product'
@@ -51,3 +53,20 @@ export const getRecentOrders = async (limit = 10) => {
}
export const toggleGateway = async () => await useAxios.post('/admin/gateway')
export const getProfitConversions = async (startDate?: string, endDate?: string) =>
await useAxios.post<ProfitConversionsResponse>('/admin/dashboard/profit-conversions', {
startDate,
endDate,
})
export const storeProfitConversion = async (payload: {
rial_amount?: number | null
gram_amount?: number | null
unit_price?: number | null
converted_at?: string | null
description?: string | null
}) => await useAxios.post<ProfitConversion>('/admin/dashboard/profit-conversions/store', payload)
export const deleteProfitConversion = async (id: number) =>
await useAxios.delete(`/admin/dashboard/profit-conversions/${id}`)
+36
View File
@@ -31,6 +31,17 @@ export interface DashboardMaster {
}>
}
export interface DashboardDailyProfit {
date: string
/** سود طلایی روز: کارمزد کسرشده از طلا (گرم) */
profit_gold: number
/** سود ریالی روز: کارمزد کسرشده از ریال */
profit_rial: number
/** سود اختلاف قیمت روز (ریال) */
spread_rial: number
order_count: number
}
export interface DashboardFinance {
fullPriceM1?: number
fullPriceM2?: number
@@ -43,6 +54,31 @@ export interface DashboardFinance {
Gifts?: string
GiftsPrise?: number
sumAll?: number
/** سود طلایی: مجموع کارمزدهای کسرشده از طلا (گرم) */
profitGoldGrams?: number
/** سود ریالی: مجموع کارمزدهای کسرشده از ریال */
profitRial?: number
/** ارزش ریالی سود طلایی با قیمت روز */
profitGoldValueRial?: number
/** تفکیک روزانهٔ سود طلایی/ریالی */
daily?: DashboardDailyProfit[]
}
export interface ProfitConversion {
id: number
user_id?: number | null
rial_amount: number
gram_amount: number
unit_price: number
converted_at?: string | null
description?: string | null
created_at?: string
}
export interface ProfitConversionsResponse {
items: ProfitConversion[]
totalRial: number
totalGram: number
}
export interface DashboardFlowPoint {
@@ -0,0 +1,503 @@
<template>
<section class="dashboard-panel dashboard-profit-split">
<div class="mb-2 flex items-start justify-between gap-2">
<DashboardSectionHeader
class="!mb-0 min-w-0 flex-1"
title="سود طلایی و ریالی"
help="کارمزد معاملات بر اساس فیلدی که مشتری وارد کرده تفکیک می‌شود: وقتی مشتری مبلغ ریالی ثابت می‌زند کارمزد از طلا کسر می‌شود (سود طلایی)، و وقتی وزن ثابت می‌زند کارمزد از ریال کسر می‌شود (سود ریالی)."
/>
<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>
<!-- جدول روزانه -->
<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>
<!-- سندهای تبدیل سود به طلا -->
<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 v-if="conversions.length" class="dashboard-text-muted text-xs">
جمع تبدیل‌شده: {{ 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)"
>
<a-button type="text" danger size="small">حذف</a-button>
</a-popconfirm>
</li>
</ul>
</a-spin>
</div>
<!-- مدال ثبت سند -->
<BasicActionSheet
v-model:visible="sanadVisible"
title="ثبت سند تبدیل سود به طلا"
modal-width="24rem"
drawer-height="auto"
compact
:show-button="false"
@close="resetForm"
>
<div class="dashboard-profit-split__form">
<div class="dashboard-profit-split__field">
<label>مبلغ ریالی سود</label>
<a-input-number
v-model:value="form.rial_amount"
:min="0"
:step="1000000"
class="w-full"
:formatter="numFormatter"
:parser="numParser"
@change="syncFromRial"
/>
</div>
<div class="dashboard-profit-split__field">
<label>قیمت هر گرم (مظنه)</label>
<a-input-number
v-model:value="form.unit_price"
:min="0"
:step="1000000"
class="w-full"
:formatter="numFormatter"
:parser="numParser"
@change="syncFromRial"
/>
</div>
<div class="dashboard-profit-split__field">
<label>وزن معادل (گرم)</label>
<a-input-number
v-model:value="form.gram_amount"
:min="0"
:step="0.1"
class="w-full"
@change="syncFromGram"
/>
</div>
<div class="dashboard-profit-split__field">
<label>تاریخ سند</label>
<BaseDatePicker
v-model:value="form.converted_at"
format="YYYY-MM-DD HH:mm:ss"
class="w-full"
/>
</div>
<div class="dashboard-profit-split__field">
<label>توضیحات</label>
<a-textarea
v-model:value="form.description"
:rows="2"
placeholder="مثلاً: از فلان‌جا ۲ گرم طلا خریده شد"
/>
</div>
<div class="dashboard-profit-split__form-footer">
<a-button danger block @click="closeSanad">بستن</a-button>
<a-button type="primary" block :loading="submitting" @click="submitSanad">ثبت سند</a-button>
</div>
</div>
</BasicActionSheet>
</section>
</template>
<script setup lang="ts">
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 BasicActionSheet from '@/components/BasicActionSheet.vue'
import { formatDashboardAmount } from '@/pages/dashboard/dashboardFormat'
import {
deleteProfitConversion,
getProfitConversions,
storeProfitConversion,
} from '@/api/dashboard'
import type { DashboardFinance, ProfitConversion } from '@/model/dashboard'
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 conversions = ref<ProfitConversion[]>([])
const conversionsLoading = ref(false)
const conversionsTotalRial = ref(0)
const conversionsTotalGram = ref(0)
const sanadVisible = ref(false)
const submitting = ref(false)
type SanadForm = {
rial_amount: number | null
gram_amount: number | null
unit_price: number | null
converted_at: string | null
description: string | null
}
function emptyForm(): SanadForm {
return {
rial_amount: null,
gram_amount: null,
unit_price: props.unitPrice ? Number(props.unitPrice) : null,
converted_at: moment().format('YYYY-MM-DD HH:mm:ss'),
description: null,
}
}
const form = ref<SanadForm>(emptyForm())
function formatGram(value?: number | null) {
return Number(value ?? 0).toLocaleString('fa-IR', { maximumFractionDigits: 4 })
}
function jalaliDate(value?: string | null) {
if (!value) return '—'
const normalized = String(value).length <= 10 ? `${value} 12:00:00` : value
return toJalali(normalized, 'jYYYY/jMM/jDD')
}
function numFormatter(value: string | number) {
return String(value ?? '').replace(/\B(?=(\d{3})+(?!\d))/g, ',')
}
function numParser(value: string) {
return value.replace(/[^\d.]/g, '')
}
function syncFromRial() {
const rial = Number(form.value.rial_amount ?? 0)
const unit = Number(form.value.unit_price ?? 0)
if (rial > 0 && unit > 0) {
form.value.gram_amount = Number((rial / unit).toFixed(4))
}
}
function syncFromGram() {
const gram = Number(form.value.gram_amount ?? 0)
const unit = Number(form.value.unit_price ?? 0)
if (gram > 0 && unit > 0) {
form.value.rial_amount = Math.round(gram * unit)
}
}
async function loadConversions() {
conversionsLoading.value = true
try {
const res = await getProfitConversions(props.startDate, props.endDate)
conversions.value = res?.items ?? []
conversionsTotalRial.value = Number(res?.totalRial ?? 0)
conversionsTotalGram.value = Number(res?.totalGram ?? 0)
} catch (e) {
console.error(e)
} finally {
conversionsLoading.value = false
}
}
function openSanad() {
form.value = emptyForm()
sanadVisible.value = true
}
function closeSanad() {
sanadVisible.value = false
resetForm()
}
function resetForm() {
form.value = emptyForm()
}
async function submitSanad() {
const rial = Number(form.value.rial_amount ?? 0)
const gram = Number(form.value.gram_amount ?? 0)
if (rial <= 0 && gram <= 0) {
message.warning('مبلغ ریالی یا وزن طلا را وارد کنید')
return
}
submitting.value = true
try {
await storeProfitConversion({
rial_amount: form.value.rial_amount,
gram_amount: form.value.gram_amount,
unit_price: form.value.unit_price,
converted_at: form.value.converted_at,
description: form.value.description,
})
message.success('سند تبدیل سود ثبت شد')
closeSanad()
await loadConversions()
} catch (e) {
console.error(e)
message.error('خطا در ثبت سند')
} finally {
submitting.value = false
}
}
async function removeConversion(id: number) {
try {
await deleteProfitConversion(id)
message.success('سند حذف شد')
await loadConversions()
} catch (e) {
console.error(e)
message.error('خطا در حذف سند')
}
}
watch(
() => [props.startDate, props.endDate],
() => {
void loadConversions()
},
{ immediate: true },
)
</script>
<style scoped lang="less">
.dashboard-profit-split {
margin-top: 0.5rem;
}
.dashboard-profit-split__cards {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 0.5rem;
}
@media (max-width: 640px) {
.dashboard-profit-split__cards {
grid-template-columns: 1fr;
}
}
.dashboard-profit-split__card {
display: flex;
flex-direction: column;
gap: 0.15rem;
min-height: 5rem;
padding: 0.6rem 0.75rem;
border-radius: 0.5rem;
border: 1px solid var(--admin-border);
background: var(--admin-bg-muted);
}
.dashboard-profit-split__card--gold {
border-color: rgba(245, 158, 11, 0.4);
background: rgba(245, 158, 11, 0.1);
}
.dashboard-profit-split__card--rial {
border-color: rgba(14, 165, 233, 0.35);
background: rgba(14, 165, 233, 0.1);
}
.dashboard-profit-split__card-title {
font-size: 0.8125rem;
font-weight: 700;
color: var(--admin-text);
}
.dashboard-profit-split__card-hint {
margin: 0;
font-size: 0.625rem;
color: var(--admin-text-secondary);
}
.dashboard-profit-split__value {
margin: 0.15rem 0 0;
direction: ltr;
text-align: right;
unicode-bidi: isolate;
}
.dashboard-profit-split__amount {
font-size: 1.05rem;
font-weight: 800;
font-variant-numeric: tabular-nums;
}
.dashboard-profit-split__unit {
margin-right: 3px;
font-size: 0.625rem;
font-weight: 600;
color: var(--admin-text-secondary);
}
.dashboard-profit-split__sub {
margin: 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;
}
.dashboard-profit-split__sanad-list {
display: flex;
flex-direction: column;
gap: 0.4rem;
margin: 0;
padding: 0;
list-style: none;
}
.dashboard-profit-split__sanad-item {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.5rem;
padding: 0.5rem 0.65rem;
border-radius: 0.5rem;
background: var(--admin-bg-muted);
border: 1px solid var(--admin-border);
}
.dashboard-profit-split__form {
@apply flex flex-col gap-3 px-4 pb-2 pt-1;
}
.dashboard-profit-split__field {
@apply flex flex-col gap-1;
}
.dashboard-profit-split__field label {
@apply text-xs font-semibold text-slate-600 dark:text-slate-300;
}
.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;
}
.dashboard-text-muted {
color: var(--admin-text-secondary);
}
.dashboard-empty {
color: var(--admin-text-secondary);
}
.dashboard-profit {
color: #059669;
}
html.is-dark {
.dashboard-profit {
color: #6ee7b7;
}
.dashboard-profit-split__card--gold {
border-color: rgba(245, 158, 11, 0.45);
background: rgba(245, 158, 11, 0.14);
}
.dashboard-profit-split__card--rial {
border-color: rgba(14, 165, 233, 0.4);
background: rgba(14, 165, 233, 0.14);
}
}
</style>
+8
View File
@@ -47,6 +47,13 @@
<DashboardTradingIncomeSummary :finance="finance" />
<DashboardProfitSplit
:finance="finance"
:start-date="startDate"
:end-date="endDate"
:unit-price="master?.price"
/>
<!-- تراز و آمار — ۴ کارت در یک ردیف -->
<section class="dashboard-metrics-row mt-2">
<DashboardStatCard
@@ -446,6 +453,7 @@ import DashboardQuickWalletModal from '@/pages/dashboard/components/DashboardQui
import DashboardSectionHeader from '@/pages/dashboard/components/DashboardSectionHeader.vue'
import DashboardStatCard from '@/pages/dashboard/components/DashboardStatCard.vue'
import DashboardTradingIncomeSummary from '@/pages/dashboard/components/DashboardTradingIncomeSummary.vue'
import DashboardProfitSplit from '@/pages/dashboard/components/DashboardProfitSplit.vue'
import {
dashboardCharts,
dashboardCustomerMetrics,