added rahimi dashboard

This commit is contained in:
2026-08-03 22:22:39 +03:30
parent 82e72b9811
commit 45647e7266
9 changed files with 1045 additions and 74 deletions
+7
View File
@@ -5,6 +5,7 @@ import type {
DashboardFinance,
DashboardMaster,
DashboardOrderPeakHours,
MontzarReconcilePayload,
PriceChartPoint,
ProfitConversion,
ProfitConversionsResponse,
@@ -70,3 +71,9 @@ export const storeProfitConversion = async (payload: {
export const deleteProfitConversion = async (id: number) =>
await useAxios.delete(`/admin/dashboard/profit-conversions/${id}`)
export const getMontzarReconcile = async (startDate: string, endDate: string) =>
await useAxios.post<MontzarReconcilePayload>('/admin/dashboard/montzar-reconcile', {
startDate: startDate.slice(0, 10),
endDate: endDate.slice(0, 10),
})
+11 -4
View File
@@ -20,6 +20,8 @@ html {
--admin-shadow: rgba(0, 0, 0, 0.06);
--admin-menu-selected: @gold-color-muted;
--admin-menu-selected-text: @gold-color-dark;
/* متن روی دکمه/سطح طلایی — سفید روی طلا خوانا نیست */
--admin-on-primary: #141414;
--admin-gradient-surface: #ffffff;
--admin-gradient-surface-fade: rgba(255, 255, 255, 0.85);
}
@@ -44,6 +46,7 @@ html.is-dark {
--admin-shadow: rgba(0, 0, 0, 0.45);
--admin-menu-selected: @gold-color-light;
--admin-menu-selected-text: #141414;
--admin-on-primary: #141414;
--admin-gradient-surface: #141414;
--admin-gradient-surface-fade: rgba(20, 20, 20, 0.85);
@@ -70,20 +73,24 @@ body {
line-height: 1 !important;
}
.ant-btn-primary {
.ant-btn-primary:not(.ant-btn-dangerous) {
background: var(--admin-gold-light) !important;
border-color: var(--admin-gold-light) !important;
color: var(--admin-on-primary) !important;
text-shadow: none !important;
}
.ant-btn-primary:not([disabled]):hover,
.ant-btn-primary:not([disabled]):focus {
.ant-btn-primary:not(.ant-btn-dangerous):not([disabled]):hover,
.ant-btn-primary:not(.ant-btn-dangerous):not([disabled]):focus {
background: var(--admin-gold-hover) !important;
border-color: var(--admin-gold-hover) !important;
color: var(--admin-on-primary) !important;
}
.ant-btn-primary:not([disabled]):active {
.ant-btn-primary:not(.ant-btn-dangerous):not([disabled]):active {
background: var(--admin-gold) !important;
border-color: var(--admin-gold) !important;
color: var(--admin-on-primary) !important;
}
.ant-menu-horizontal:not(.ant-menu-dark) > .ant-menu-item-selected,
+8 -5
View File
@@ -80,13 +80,13 @@ html.is-dark .ant-picker-focused {
box-shadow: 0 0 0 2px rgba(212, 175, 55, 0.18) !important;
}
html.is-dark .ant-radio-group-solid .ant-radio-button-wrapper-checked:not(.ant-radio-button-wrapper-disabled) {
color: var(--admin-menu-selected-text) !important;
color: var(--admin-on-primary) !important;
background: var(--admin-gold-light) !important;
border-color: var(--admin-gold-light) !important;
}
html.is-dark .ant-radio-group-solid .ant-radio-button-wrapper-checked:not(.ant-radio-button-wrapper-disabled):hover,
html.is-dark .ant-radio-group-solid .ant-radio-button-wrapper-checked:not(.ant-radio-button-wrapper-disabled):active {
color: var(--admin-menu-selected-text) !important;
color: var(--admin-on-primary) !important;
background: var(--admin-gold-hover) !important;
border-color: var(--admin-gold-hover) !important;
}
@@ -105,14 +105,17 @@ html.is-dark .ant-radio-button-wrapper-checked:not(.ant-radio-button-wrapper-dis
html.is-dark .ant-radio-button-wrapper-checked:not(.ant-radio-button-wrapper-disabled)::before {
background-color: var(--admin-gold-light) !important;
}
html.is-dark .ant-btn-primary {
html.is-dark .ant-btn-primary:not(.ant-btn-dangerous) {
background: var(--admin-gold-light) !important;
border-color: var(--admin-gold-light) !important;
color: var(--admin-on-primary) !important;
text-shadow: none !important;
}
html.is-dark .ant-btn-primary:not([disabled]):hover,
html.is-dark .ant-btn-primary:not([disabled]):focus {
html.is-dark .ant-btn-primary:not(.ant-btn-dangerous):not([disabled]):hover,
html.is-dark .ant-btn-primary:not(.ant-btn-dangerous):not([disabled]):focus {
background: var(--admin-gold-hover) !important;
border-color: var(--admin-gold-hover) !important;
color: var(--admin-on-primary) !important;
}
html.is-dark .ant-btn-default:not([disabled]):hover,
html.is-dark .ant-btn-default:not([disabled]):focus {
+97
View File
@@ -143,3 +143,100 @@ export interface DashboardWalletSums {
cash?: number
cashToCash?: number
}
export interface MontzarReconcileMandeh {
gold: number
cash: number
gold_abs: number
cash_abs: number
gold_state: string
rial_state: string
gold_unit: string
cash_unit: string
}
export interface MontzarSiteSummary {
buy_gold: number
sell_gold: number
buy_rial: number
sell_rial: number
net_gold: number
net_rial: number
}
export interface MontzarColleagueRow {
index?: number
id?: number | string | null
doc_id?: number | string | null
date?: string | null
date_ymd?: string | null
desc?: string
kind?: string
bed_gold?: number
bes_gold?: number
bed_rial?: number
bes_rial?: number
net_gold?: number
net_rial?: number
remain_gold?: number
remain_rial?: number
gold_state?: string | null
rial_state?: string | null
}
export interface MontzarLocalItem {
id: number
type: string
direction?: number
direction_label?: string
amount: number
cash: number
montzar_cash?: number
covered?: boolean
provider_id?: number | string | null
product_id?: number | string | null
product_title?: string | null
customer_name?: string | null
note?: string
created_at?: string | null
date_ymd?: string | null
}
export interface MontzarReconcileDiff {
code: string
reason: string
colleague?: MontzarColleagueRow | null
local?: MontzarLocalItem | null
}
export interface MontzarReconcilePayload {
mandeh: MontzarReconcileMandeh | null
colleague_available?: boolean
colleague_error?: string | null
last_table_remain?: { gold: number; rial: number } | null
summary: {
local_all: MontzarSiteSummary
local_covered: MontzarSiteSummary
local_uncovered: MontzarSiteSummary
local_matched: MontzarSiteSummary
colleague_site: MontzarSiteSummary
diff_site_gold: number
diff_site_rial: number
order_count: number
covered_count: number
uncovered_count: number
matched_count: number
diff_count: number
gift_count?: number
colleague_row_count: number
local_order_count: number
}
colleague_rows: MontzarColleagueRow[]
local_orders: MontzarLocalItem[]
local_uncovered_orders?: MontzarLocalItem[]
local_gifts: MontzarLocalItem[]
matched: Array<{ code: string; colleague: MontzarColleagueRow; local: MontzarLocalItem }>
diffs: MontzarReconcileDiff[]
colleague_other?: MontzarReconcileDiff[]
range: { start: string; end: string }
}
@@ -0,0 +1,320 @@
<template>
<div class="income-mix">
<DashboardSectionHeader title="ترکیب درآمد" :help="help" />
<div v-if="total <= 0" class="income-mix__empty">
در این بازه داده مالی ثبت نشده
</div>
<div v-else class="income-mix__body">
<div class="income-mix__chart">
<div class="income-mix__donut">
<apexchart
type="donut"
width="100%"
height="168"
:options="chartOptions"
:series="chartSeries"
/>
<div class="income-mix__center">
<span class="income-mix__center-label">جمع سود</span>
<span class="income-mix__center-amount" dir="ltr">
<bdi>{{ compactTotal }}</bdi>
</span>
<span class="income-mix__center-unit">{{ DASHBOARD_RIAL_UNIT }}</span>
</div>
</div>
</div>
<ul class="income-mix__list">
<li v-for="row in rows" :key="row.key" class="income-mix__row">
<span class="income-mix__dot" :style="{ background: row.color }" />
<div class="income-mix__meta">
<div class="income-mix__name-row">
<span class="income-mix__name">{{ row.label }}</span>
<span class="income-mix__pct-pill" :style="{ color: row.color, background: row.tint }">
{{ row.pctLabel }}
</span>
</div>
<span class="income-mix__hint">{{ row.hint }}</span>
<span class="income-mix__amount" dir="ltr">
<bdi>{{ formatDashboardAmount(row.value) }}</bdi>
<span class="income-mix__unit">{{ DASHBOARD_RIAL_UNIT }}</span>
</span>
</div>
</li>
</ul>
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import DashboardSectionHeader from '@/pages/dashboard/components/DashboardSectionHeader.vue'
import { dashboardCharts } from '@/pages/dashboard/dashboardSectionCopy'
import { DASHBOARD_RIAL_UNIT, formatDashboardAmount } from '@/pages/dashboard/dashboardFormat'
import { useAdminTheme } from '@core/composable/useAdminTheme'
import type { DashboardFinance } from '@/model/dashboard'
const props = defineProps<{
finance?: DashboardFinance | null
}>()
const { isDark } = useAdminTheme()
const help = dashboardCharts.incomeMix
const sood = computed(() => Number(props.finance?.fullPriceSood ?? 0))
const karmozd = computed(
() => Number(props.finance?.fullPriceKarmozd ?? props.finance?.fullPriceKaermozd ?? 0),
)
const total = computed(() => sood.value + karmozd.value)
function tint(hex: string, dark: boolean) {
return dark ? `${hex}33` : `${hex}1a`
}
const rows = computed(() => {
const t = total.value
const dark = isDark.value
const items = [
{
key: 'sood',
label: 'سود اختلاف قیمت',
hint: 'فاصله مظنه و قیمت مشتری',
value: sood.value,
color: '#10b981',
},
{
key: 'fee',
label: 'کارمزد معامله',
hint: 'کارمزد گرفته‌شده از سفارش‌ها',
value: karmozd.value,
color: '#f59e0b',
},
]
return items.map((item) => {
const pct = t > 0 ? Math.round((item.value / t) * 1000) / 10 : 0
return {
...item,
pct,
pctLabel: `${pct.toLocaleString('fa-IR')}٪`,
tint: tint(item.color, dark),
}
})
})
const chartSeries = computed(() => rows.value.map((r) => r.value))
const compactTotal = computed(() => {
const n = total.value
if (n >= 1_000_000_000) return `${(n / 1_000_000_000).toFixed(1)}B`
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`
return formatDashboardAmount(n)
})
const chartOptions = computed(() => {
const dark = isDark.value
return {
chart: {
type: 'donut',
fontFamily: 'inherit',
foreColor: dark ? 'rgba(255,255,255,0.55)' : 'rgba(0,0,0,0.55)',
animations: { enabled: true, speed: 450 },
},
theme: { mode: dark ? 'dark' : 'light' },
labels: rows.value.map((r) => r.label),
colors: rows.value.map((r) => r.color),
stroke: { width: 0 },
legend: { show: false },
dataLabels: { enabled: false },
plotOptions: {
pie: {
expandOnClick: false,
donut: {
size: '74%',
labels: { show: false },
},
},
},
tooltip: {
theme: dark ? 'dark' : 'light',
y: {
formatter: (v: number) => `${formatDashboardAmount(v)} ${DASHBOARD_RIAL_UNIT}`,
},
},
states: {
hover: { filter: { type: 'lighten', value: 0.08 } },
active: { filter: { type: 'none' } },
},
}
})
</script>
<style scoped lang="less">
.income-mix {
color: var(--admin-text);
}
.income-mix__empty {
display: flex;
align-items: center;
justify-content: center;
min-height: 11rem;
font-size: 0.75rem;
color: var(--admin-text-secondary);
}
.income-mix__body {
display: grid;
grid-template-columns: minmax(140px, 168px) minmax(0, 1fr);
gap: 0.75rem 1rem;
align-items: center;
}
@media (max-width: 520px) {
.income-mix__body {
grid-template-columns: 1fr;
justify-items: center;
}
.income-mix__list {
width: 100%;
}
}
.income-mix__chart {
width: 100%;
max-width: 168px;
}
.income-mix__donut {
position: relative;
width: 100%;
aspect-ratio: 1;
}
.income-mix__donut :deep(.apexcharts-canvas) {
margin: 0 auto;
}
.income-mix__center {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
pointer-events: none;
padding: 1.25rem;
text-align: center;
}
.income-mix__center-label {
font-size: 0.625rem;
font-weight: 700;
color: var(--admin-text-secondary);
line-height: 1.2;
}
.income-mix__center-amount {
margin-top: 0.15rem;
font-size: 0.95rem;
font-weight: 800;
font-variant-numeric: tabular-nums;
color: var(--admin-text);
line-height: 1.2;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.income-mix__center-unit {
margin-top: 0.1rem;
font-size: 0.5625rem;
font-weight: 600;
color: var(--admin-text-secondary);
}
.income-mix__list {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 0.5rem;
min-width: 0;
}
.income-mix__row {
display: grid;
grid-template-columns: 0.55rem minmax(0, 1fr);
gap: 0.55rem;
align-items: start;
padding: 0.55rem 0.6rem;
border-radius: 0.6rem;
border: 1px solid var(--admin-border);
background: var(--admin-bg-muted);
}
.income-mix__dot {
width: 0.55rem;
height: 0.55rem;
margin-top: 0.3rem;
border-radius: 999px;
flex-shrink: 0;
box-shadow: 0 0 0 3px color-mix(in srgb, currentColor 12%, transparent);
}
.income-mix__meta {
min-width: 0;
display: flex;
flex-direction: column;
gap: 0.15rem;
}
.income-mix__name-row {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.35rem;
}
.income-mix__name {
font-size: 0.75rem;
font-weight: 700;
line-height: 1.35;
}
.income-mix__pct-pill {
font-size: 0.625rem;
font-weight: 800;
line-height: 1;
padding: 0.2rem 0.4rem;
border-radius: 999px;
font-variant-numeric: tabular-nums;
}
.income-mix__hint {
font-size: 0.625rem;
line-height: 1.4;
color: var(--admin-text-secondary);
}
.income-mix__amount {
margin-top: 0.1rem;
font-size: 0.8125rem;
font-weight: 800;
font-variant-numeric: tabular-nums;
display: inline-flex;
align-items: baseline;
gap: 0.2rem;
}
.income-mix__unit {
font-size: 0.625rem;
font-weight: 600;
color: var(--admin-text-secondary);
}
</style>
@@ -0,0 +1,584 @@
<template>
<section v-if="visible" class="montzar-simple">
<DashboardSectionHeader title="آبشده و همکار" :help="sectionHelp" />
<a-spin :spinning="loading">
<div v-if="error" class="montzar-simple__error">
{{ error }}
<a-button class="mt-2" size="small" @click="load">تلاش مجدد</a-button>
</div>
<template v-else-if="data">
<div class="montzar-simple__grid">
<article class="montzar-simple__card">
<span class="montzar-simple__label">خرید مشتری</span>
<p class="montzar-simple__hint">آبشده در این بازه</p>
<p class="montzar-simple__value montzar-simple__value--inline">
<bdi class="montzar-simple__amount number-ltr">{{ formatGram(data.summary.local_all.buy_gold) }}</bdi>
<span class="montzar-simple__unit">گرم</span>
</p>
</article>
<article class="montzar-simple__card">
<span class="montzar-simple__label">فروش مشتری</span>
<p class="montzar-simple__hint">آبشده در این بازه</p>
<p class="montzar-simple__value montzar-simple__value--inline">
<bdi class="montzar-simple__amount number-ltr">{{ formatGram(data.summary.local_all.sell_gold) }}</bdi>
<span class="montzar-simple__unit">گرم</span>
</p>
</article>
<article class="montzar-simple__card">
<span class="montzar-simple__label">ثبت‌شده نزد همکار</span>
<p class="montzar-simple__hint">از همکار خرید یا فروش شده</p>
<div class="montzar-simple__value montzar-simple__value--stack">
<p class="montzar-simple__ratio">
<bdi class="montzar-simple__amount">{{ data.summary.covered_count }}</bdi>
<span class="montzar-simple__sep">از</span>
<bdi class="montzar-simple__amount montzar-simple__amount--muted">{{ data.summary.order_count }}</bdi>
</p>
<span class="montzar-simple__unit">سفارش</span>
</div>
</article>
<article
class="montzar-simple__card"
:class="data.summary.uncovered_count ? 'montzar-simple__card--bad' : 'montzar-simple__card--ok'"
>
<span class="montzar-simple__label">
{{ data.summary.uncovered_count ? 'ثبت‌نشده نزد همکار' : 'ثبت‌نشده نزد همکار' }}
</span>
<p class="montzar-simple__hint">
{{
data.summary.uncovered_count
? 'در اپ هست ولی از همکار خرید/فروش نشده'
: 'همه سفارش‌ها نزد همکار ثبت شده‌اند'
}}
</p>
<div class="montzar-simple__value montzar-simple__value--stack">
<p class="montzar-simple__ratio">
<bdi class="montzar-simple__amount">{{ data.summary.uncovered_count }}</bdi>
<span class="montzar-simple__unit">سفارش</span>
</p>
<p v-if="uncoveredGold > 0" class="montzar-simple__sub-line">
<bdi class="number-ltr">{{ formatGram(uncoveredGold) }}</bdi>
<span>گرم</span>
</p>
</div>
</article>
</div>
<div class="montzar-simple__actions">
<a-button
v-if="data.summary.uncovered_count"
type="primary"
size="small"
danger
@click="openTab('uncovered')"
>
ببین کدام سفارش‌ها نزد همکار ثبت نشده
</a-button>
<a-button size="small" @click="openTab('orders')">همه سفارش‌ها</a-button>
<a-button size="small" type="link" class="!px-1" @click="showMore = !showMore">
{{ showMore ? 'بستن جزئیات' : 'جزئیات بیشتر' }}
</a-button>
<a-button size="small" @click="load">بروزرسانی</a-button>
</div>
<div v-if="showMore" class="montzar-simple__more">
<p v-if="data.mandeh" class="montzar-simple__more-line">
مانده حساب نزد همکار الان:
<b class="number-ltr" dir="ltr">
{{ formatGram(data.mandeh.gold_abs) }} گرم ·
{{ formatDashboardAmount(data.mandeh.cash_abs) }} ریال
</b>
({{ data.mandeh.gold_state }} / {{ data.mandeh.rial_state }})
</p>
<p v-if="data.colleague_available" class="montzar-simple__more-line">
اختلاف گرم دفتر همکار با سفارش‌های ثبت‌شده این بازه:
<b class="number-ltr" dir="ltr">{{ formatSignedGram(data.summary.diff_site_gold) }} گرم</b>
</p>
<div class="montzar-simple__actions mt-1">
<a-button
v-if="data.summary.diff_count"
size="small"
danger
@click="openTab('diffs')"
>
موارد اختلاف ({{ data.summary.diff_count }})
</a-button>
<a-button
v-if="data.colleague_available"
size="small"
@click="openTab('colleague')"
>
ریز دفتر همکار
</a-button>
</div>
</div>
</template>
</a-spin>
<BasicModal
v-model:visible="modalOpen"
:title="modalTitle"
width="920px"
:custom-footer="true"
@close="modalOpen = false"
>
<div v-if="activeTab === 'uncovered'" class="overflow-x-auto">
<p class="montzar-simple__modal-intro">
این سفارش‌ها در اپ ثبت شده‌اند ولی از همکار خرید یا فروش نشده‌اند.
</p>
<table class="montzar-simple__table">
<thead>
<tr>
<th>شناسه</th>
<th>زمان</th>
<th>خرید/فروش</th>
<th>مشتری</th>
<th>گرم</th>
<th>ریال</th>
</tr>
</thead>
<tbody>
<tr v-for="row in uncoveredOrders" :key="`u-${row.id}`">
<td>{{ row.id }}</td>
<td class="whitespace-nowrap">{{ row.created_at }}</td>
<td>{{ row.direction_label }}</td>
<td>{{ row.customer_name || '—' }}</td>
<td class="number-ltr" dir="ltr">{{ formatGram(row.amount) }}</td>
<td class="number-ltr" dir="ltr">{{ formatDashboardAmount(row.cash) }}</td>
</tr>
</tbody>
</table>
<div v-if="!uncoveredOrders.length" class="montzar-simple__empty">موردی نیست</div>
</div>
<div v-else-if="activeTab === 'orders'" class="overflow-x-auto">
<table class="montzar-simple__table">
<thead>
<tr>
<th>شناسه</th>
<th>زمان</th>
<th>خرید/فروش</th>
<th>مشتری</th>
<th>گرم</th>
<th>نزد همکار؟</th>
</tr>
</thead>
<tbody>
<tr v-for="row in data?.local_orders ?? []" :key="`o-${row.id}`">
<td>{{ row.id }}</td>
<td class="whitespace-nowrap">{{ row.created_at }}</td>
<td>{{ row.direction_label }}</td>
<td>{{ row.customer_name || '—' }}</td>
<td class="number-ltr" dir="ltr">{{ formatGram(row.amount) }}</td>
<td>{{ row.covered ? 'بله' : 'خیر' }}</td>
</tr>
</tbody>
</table>
</div>
<div v-else-if="activeTab === 'colleague'" class="overflow-x-auto">
<table class="montzar-simple__table">
<thead>
<tr>
<th>تاریخ</th>
<th>شرح</th>
<th>نوع</th>
<th>طلا</th>
<th>ریال</th>
</tr>
</thead>
<tbody>
<tr
v-for="(row, i) in data?.colleague_rows ?? []"
:key="`c-${row.id ?? i}`"
>
<td class="whitespace-nowrap">{{ row.date }}</td>
<td>{{ row.desc || '—' }}</td>
<td>{{ kindLabel(row.kind) }}</td>
<td class="number-ltr" dir="ltr">{{ formatSignedGram(row.net_gold ?? 0) }}</td>
<td class="number-ltr" dir="ltr">{{ formatSignedAmount(row.net_rial ?? 0) }}</td>
</tr>
</tbody>
</table>
</div>
<div v-else class="space-y-2">
<div
v-for="(diff, i) in data?.diffs ?? []"
:key="`d-${i}`"
class="montzar-simple__diff"
>
<b>{{ diffCodeLabel(diff.code) }}</b>
<p class="m-0 mt-1">{{ diff.reason }}</p>
<div v-if="diff.local" class="montzar-simple__diff-meta">
سفارش اپ شماره {{ diff.local.id }} · {{ formatGram(diff.local.amount) }} گرم
</div>
<div v-if="diff.colleague" class="montzar-simple__diff-meta">
دفتر همکار: {{ diff.colleague.desc || '—' }} ·
{{ formatSignedGram(diff.colleague.net_gold ?? 0) }} گرم
</div>
</div>
<div v-if="!(data?.diffs?.length)" class="montzar-simple__empty">مورد اختلافی نیست</div>
</div>
<template #footer>
<a-button @click="modalOpen = false">بستن</a-button>
</template>
</BasicModal>
</section>
</template>
<script setup lang="ts">
import { computed, ref, watch } from 'vue'
import BasicModal from '@/components/BasicModal.vue'
import DashboardSectionHeader from '@/pages/dashboard/components/DashboardSectionHeader.vue'
import { getMontzarReconcile } from '@/api/dashboard'
import type { DashboardHelp } from '@/pages/dashboard/dashboardSectionCopy'
import { formatDashboardAmount } from '@/pages/dashboard/dashboardFormat'
import type { MontzarLocalItem, MontzarReconcilePayload } from '@/model/dashboard'
import { priceProviderSetting } from '@/utils/config'
const props = defineProps<{
startDate: string
endDate: string
}>()
const visible = computed(() => priceProviderSetting.value === 'montzar')
const loading = ref(false)
const error = ref('')
const data = ref<MontzarReconcilePayload | null>(null)
const modalOpen = ref(false)
const showMore = ref(false)
const activeTab = ref<'uncovered' | 'orders' | 'colleague' | 'diffs'>('uncovered')
const sectionHelp: DashboardHelp = {
description:
'هر خرید یا فروش آبشده مشتری باید نزد همکار هم ثبت شود. اینجا می‌بینید چند تا ثبت شده و چند تا جا مانده.',
formula: 'ثبت‌نشده = سفارش موفق آبشده بدون شناسه ثبت نزد همکار',
}
const uncoveredGold = computed(() => {
const u = data.value?.summary.local_uncovered
if (!u) return 0
return Number(u.buy_gold || 0) + Number(u.sell_gold || 0)
})
const uncoveredOrders = computed<MontzarLocalItem[]>(() => {
if (data.value?.local_uncovered_orders?.length) {
return data.value.local_uncovered_orders
}
return (data.value?.local_orders ?? []).filter((o) => !o.covered)
})
const modalTitle = computed(() => {
if (activeTab.value === 'uncovered') return 'سفارش‌هایی که نزد همکار ثبت نشده‌اند'
if (activeTab.value === 'orders') return 'همه خرید و فروش آبشده'
if (activeTab.value === 'diffs') return 'موارد اختلاف'
return 'ریز دفتر همکار'
})
function formatGram(value?: number) {
return Number(value ?? 0).toLocaleString('en-US', { maximumFractionDigits: 3 })
}
function formatSignedGram(value?: number) {
const n = Number(value ?? 0)
return `${n > 0 ? '+' : ''}${formatGram(n)}`
}
function formatSignedAmount(value?: number) {
const n = Math.round(Number(value ?? 0))
return `${n > 0 ? '+' : ''}${formatDashboardAmount(Math.abs(n))}`
}
function kindLabel(kind?: string) {
const map: Record<string, string> = {
site_sell: 'فروش از سایت',
site_buy: 'خرید از سایت',
settlement: 'تسویه',
physical: 'فیزیکی',
gift: 'پاداش',
other: 'سایر',
}
return map[kind ?? ''] ?? kind ?? '—'
}
function diffCodeLabel(code: string) {
const map: Record<string, string> = {
local_uncovered: 'در اپ هست، نزد همکار ثبت نشده',
colleague_site_only: 'فقط در دفتر همکار هست',
local_covered_not_in_ledger: 'نزد همکار ثبت شده، در دفتر بازه نیست',
gift_uncovered: 'پاداش بدون ثبت نزد همکار',
}
return map[code] ?? code
}
function openTab(tab: 'uncovered' | 'orders' | 'colleague' | 'diffs') {
activeTab.value = tab
modalOpen.value = true
}
async function load() {
if (!visible.value) {
data.value = null
return
}
loading.value = true
error.value = ''
try {
data.value = (await getMontzarReconcile(props.startDate, props.endDate)) as MontzarReconcilePayload
} catch (e: unknown) {
data.value = null
error.value = String(
(e as { response?: { data?: { message?: string } }; message?: string })?.response?.data
?.message
|| (e as { message?: string })?.message
|| 'خطا در محاسبه'
)
} finally {
loading.value = false
}
}
watch(
() => [visible.value, props.startDate, props.endDate] as const,
() => {
void load()
},
{ immediate: true }
)
defineExpose({ load })
</script>
<style scoped lang="less">
.montzar-simple {
margin-top: 0.5rem;
border-radius: 0.75rem;
border: 1px solid var(--admin-border);
background: var(--admin-bg-elevated);
padding: 0.65rem 0.75rem 0.75rem;
color: var(--admin-text);
}
.montzar-simple__grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 0.5rem;
}
@media (max-width: 900px) {
.montzar-simple__grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (max-width: 480px) {
.montzar-simple__grid {
grid-template-columns: 1fr;
}
}
.montzar-simple__card {
display: flex;
flex-direction: column;
gap: 0.15rem;
min-height: 4.5rem;
padding: 0.5rem 0.6rem;
border-radius: 0.5rem;
border: 1px solid var(--admin-border);
background: var(--admin-bg-muted);
}
.montzar-simple__card--bad {
border-color: rgba(244, 63, 94, 0.45);
background: rgba(244, 63, 94, 0.1);
}
.montzar-simple__card--ok {
border-color: rgba(16, 185, 129, 0.4);
background: rgba(16, 185, 129, 0.1);
}
.montzar-simple__label {
font-size: 0.75rem;
font-weight: 700;
color: var(--admin-text);
line-height: 1.35;
}
.montzar-simple__hint {
margin: 0;
flex: 1;
font-size: 0.625rem;
line-height: 1.45;
color: var(--admin-text-secondary);
}
.montzar-simple__value {
margin: 0.15rem 0 0;
text-align: right;
}
.montzar-simple__value--inline {
display: flex;
flex-wrap: wrap;
align-items: baseline;
justify-content: flex-start;
gap: 0.25rem;
}
.montzar-simple__value--stack {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 0.1rem;
}
.montzar-simple__ratio {
margin: 0;
display: flex;
flex-wrap: wrap;
align-items: baseline;
gap: 0.25rem;
}
.montzar-simple__sep {
font-size: 0.75rem;
font-weight: 600;
color: var(--admin-text-secondary);
}
.montzar-simple__amount {
font-size: 0.9375rem;
font-weight: 800;
font-variant-numeric: tabular-nums;
color: var(--admin-text);
}
.montzar-simple__amount--muted {
font-weight: 700;
color: var(--admin-text-muted);
}
.montzar-simple__card--bad .montzar-simple__amount {
color: #e11d48;
}
.montzar-simple__card--ok .montzar-simple__amount {
color: #059669;
}
.montzar-simple__unit {
font-size: 0.625rem;
font-weight: 600;
color: var(--admin-text-secondary);
}
.montzar-simple__sub-line {
margin: 0;
display: flex;
align-items: baseline;
gap: 0.2rem;
font-size: 0.6875rem;
font-weight: 600;
color: var(--admin-text-secondary);
font-variant-numeric: tabular-nums;
}
.montzar-simple__actions {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.4rem;
margin-top: 0.65rem;
}
.montzar-simple__more {
margin-top: 0.5rem;
padding-top: 0.5rem;
border-top: 1px dashed var(--admin-border);
}
.montzar-simple__more-line {
margin: 0 0 0.35rem;
font-size: 0.75rem;
color: var(--admin-text-secondary);
line-height: 1.6;
}
.montzar-simple__more-line b {
color: var(--admin-text);
font-weight: 700;
}
.montzar-simple__error {
padding: 0.75rem;
text-align: center;
font-size: 12px;
color: #e11d48;
}
.montzar-simple__empty {
padding: 1.5rem 0;
text-align: center;
font-size: 12px;
color: var(--admin-text-secondary);
}
.montzar-simple__modal-intro {
margin: 0 0 0.75rem;
font-size: 12px;
color: var(--admin-text-secondary);
}
.montzar-simple__table {
width: 100%;
min-width: 520px;
border-collapse: collapse;
font-size: 12px;
color: var(--admin-text);
}
.montzar-simple__table th,
.montzar-simple__table td {
padding: 0.4rem 0.5rem;
border-bottom: 1px solid var(--admin-border);
text-align: right;
}
.montzar-simple__table th {
color: var(--admin-text-secondary);
font-weight: 600;
}
.montzar-simple__diff {
border: 1px solid var(--admin-border);
border-radius: 0.5rem;
padding: 0.6rem 0.75rem;
font-size: 12px;
background: var(--admin-bg-muted);
}
.montzar-simple__diff-meta {
margin-top: 0.25rem;
font-size: 11px;
color: var(--admin-text-secondary);
}
</style>
<style lang="less">
html.is-dark {
.montzar-simple__card--bad .montzar-simple__amount {
color: #fda4af;
}
.montzar-simple__card--ok .montzar-simple__amount {
color: #6ee7b7;
}
}
</style>
@@ -275,47 +275,6 @@ export function useDashboardPage() {
?? (order.sts === 2 ? 'موفق' : order.sts === 1 ? 'باز' : 'ناموفق')
}
const financeChartOptions = computed(() => {
const sood = finance.value?.fullPriceSood ?? 0
const karmozd = finance.value?.fullPriceKarmozd ?? finance.value?.fullPriceKaermozd ?? 0
const gifts = finance.value?.GiftsPrise ?? 0
const total = finance.value?.totalTradingProfit ?? sood + karmozd
return {
...chartSurfaceOptions(),
chart: { type: 'donut', fontFamily: 'inherit', offsetY: 4, foreColor: chartSurfaceOptions().chart.foreColor },
labels: ['سود اختلاف قیمت', 'کارمزد معامله', 'ارزش هدایا (گرم)'],
colors: ['#10b981', '#f59e0b', '#8b5cf6'],
legend: {
position: 'bottom',
fontSize: '12px',
horizontalAlign: 'center',
offsetY: 6,
itemMargin: { horizontal: 12, vertical: 4 },
},
dataLabels: { enabled: false },
plotOptions: {
pie: {
donut: {
size: '68%',
labels: {
show: true,
total: {
show: true,
label: 'جمع سود معاملات',
formatter: () => `${formatDashboardAmount(total)} ${DASHBOARD_RIAL_UNIT}`,
},
},
},
},
},
tooltip: {
y: { formatter: (v: number) => `${formatDashboardAmount(v)} ${DASHBOARD_RIAL_UNIT}` },
},
series: [sood, karmozd, gifts],
}
})
const flowChartOptions = computed(() => {
const mobile = isMobile.value
const surface = chartSurfaceOptions()
@@ -628,7 +587,6 @@ export function useDashboardPage() {
directionClass,
stsClass,
stsLabel,
financeChartOptions,
flowChartOptions,
flowChartHeight,
peakHoursChartOptions,
+11 -5
View File
@@ -88,8 +88,8 @@ export const dashboardDateMetrics = {
export const dashboardCharts = {
incomeMix: help(
'تفکیک درآمد: سود اختلاف قیمت، کارمزد ریالی، و ارزش هدایا.',
'درآمد = سود اختلاف قیمت + کارمزد + (گرم هدیه × مظنه)'
'سهم سود اختلاف قیمت و کارمزد از جمع سود معاملات در بازه انتخابی. هدایا هزینهٔ پاداش‌اند و اینجا جزو سود نیستند.',
'جمع سود = سود اختلاف قیمت + کارمزد'
),
buySellVolume: help(
'مقدار طلای خرید و فروش مشتری در هر روز (فقط سفارش موفق).',
@@ -129,12 +129,18 @@ export const dashboardCustomerMetrics = {
} as const satisfies Record<string, DashboardHelp>
export const dashboardWallet = help(
'مجموع موجودی طلا و ریال همه مشتریان.',
'مجموع موجودی طلا و ریال همه مشتریان (بدهی اپ به کاربران). این عدد دفتر اکانت همکار نیست.',
'ارزش طلا به ریال = جمع گرم × مظنه لحظه‌ای'
)
export const dashboardWalletMetrics = {
gold: help('مجموع طلای موجود در کیف همه مشتریان.', 'جمع گرم طلا در کیف‌ها'),
cash: help('مجموع ریال موجود در کیف همه مشتریان.', 'جمع ریال در کیف‌ها'),
gold: help(
'مجموع طلای موجود در کیف همه مشتریان — بدهی به مشتری؛ با ماندهٔ پنل همکار یکی نیست.',
'جمع گرم طلا در کیف‌ها'
),
cash: help(
'مجموع ریال موجود در کیف همه مشتریان — شارژ/مانده مشتری؛ با ماندهٔ ریالی همکار یکی نیست.',
'جمع ریال در کیف‌ها'
),
goldToCash: help('ارزش ریالی کل طلای مشتریان با مظنه فعلی.', 'جمع گرم × قیمت هر گرم'),
} as const satisfies Record<string, DashboardHelp>
+7 -18
View File
@@ -139,25 +139,12 @@
</div>
</section>
<DashboardMontzarReconcile :start-date="startDate" :end-date="endDate" />
<!-- Charts row 1 -->
<section class="mt-2 grid grid-cols-1 gap-2 xl:grid-cols-2">
<div class="dashboard-panel">
<DashboardSectionHeader title="ترکیب درآمد" :help="copy.charts.incomeMix" />
<div class="dashboard-chart-wrap">
<apexchart
v-if="(finance?.fullPriceSood ?? 0) + (finance?.fullPriceKarmozd ?? 0) + (finance?.GiftsPrise ?? 0) > 0"
type="donut"
height="220"
:options="financeChartOptions"
:series="financeChartOptions.series"
/>
<div
v-else
class="dashboard-empty flex h-44 items-center justify-center text-xs"
>
در این بازه داده مالی ثبت نشده
</div>
</div>
<DashboardIncomeMix :finance="finance" />
</div>
<div class="dashboard-panel">
@@ -461,6 +448,8 @@ import DashboardQuickWalletModal from '@/pages/dashboard/components/DashboardQui
import DashboardSectionHeader from '@/pages/dashboard/components/DashboardSectionHeader.vue'
import DashboardStatCard from '@/pages/dashboard/components/DashboardStatCard.vue'
import DashboardProfitSplit from '@/pages/dashboard/components/DashboardProfitSplit.vue'
import DashboardMontzarReconcile from '@/pages/dashboard/components/DashboardMontzarReconcile.vue'
import DashboardIncomeMix from '@/pages/dashboard/components/DashboardIncomeMix.vue'
import {
dashboardCharts,
dashboardCustomerMetrics,
@@ -481,12 +470,13 @@ 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'
import { initAccountingSettingsFromApi, pargarEnabledSetting } from '@/utils/config'
import { initAccountingSettingsFromApi, initStoreModeFromSettings, pargarEnabledSetting } from '@/utils/config'
const pargarEnabled = pargarEnabledSetting
onMounted(() => {
void initAccountingSettingsFromApi()
void initStoreModeFromSettings()
})
const copy = {
@@ -533,7 +523,6 @@ const {
directionClass,
stsClass,
stsLabel,
financeChartOptions,
flowChartOptions,
flowChartHeight,
peakHoursChartOptions,