Files
boursecu_admin/src/pages/dashboard/index.vue
T
2026-06-21 20:27:13 +03:30

532 lines
20 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="dashboard-v2 box-border min-h-full w-full max-w-full overflow-x-hidden p-2 md:p-3">
<a-spin :spinning="loading">
<!-- Hero -->
<section
class="rounded-xl bg-gradient-to-l from-[#0f766e] via-[#1e3a5f] to-[#0f172a] px-3 py-2 text-white shadow-md"
>
<div class="flex flex-wrap items-center justify-between gap-x-3 gap-y-1.5">
<div class="flex min-w-0 flex-wrap items-center gap-x-2 gap-y-0.5">
<DashboardLabelWithHelp
label="مظنه"
:help="copy.hero"
size="xs"
text-class="text-[11px]"
label-class="text-white/80"
/>
<span class="text-[11px] text-white/80">/گرم</span>
<span class="text-lg font-bold leading-none">{{ usePrice(master?.price ?? 0, true, '') }}</span>
<span class="text-white/35">·</span>
<span class="text-[11px] text-white/80">مثقال</span>
<span class="text-sm font-semibold leading-none">{{ usePrice(master?.pricem ?? 0, true, '') }}</span>
<span v-if="master?.update_date" class="text-[10px] text-white/55">
· {{ toJalali(master.update_date) }}
</span>
</div>
<div class="flex shrink-0 flex-wrap items-center gap-2">
<div class="flex items-center gap-1.5 leading-none rounded-lg bg-white/10 px-2 py-1 text-[11px]">
<BaseIcon icon="vuesax-linear:card" size="14" class="text-white/90" />
<DashboardLabelWithHelp
label="واریز آنلاین"
:help="copy.gateway"
size="xs"
text-class="text-[11px]"
label-class="text-white/85"
/>
<span class="font-semibold">{{ master?.is_gateway_open ? 'فعال' : 'غیرفعال' }}</span>
<a-switch size="small" :checked="master?.is_gateway_open" @change="onToggleGateway" />
</div>
<router-link :to="{ name: 'ordersMain' }">
<a-button type="primary" size="small" class="!h-7 !rounded-lg !px-3">سفارشات</a-button>
</router-link>
</div>
</div>
</section>
<!-- KPI -->
<section class="mt-2 grid grid-cols-2 items-stretch gap-2 md:grid-cols-3 xl:grid-cols-6">
<DashboardStatCard
tone="gold"
icon="vuesax-linear:money-receive"
title="سود معاملات"
:value="usePrice(finance?.fullPriceSood ?? 0, true, '')"
:help="copy.kpi.profit"
/>
<DashboardStatCard
tone="amber"
icon="vuesax-linear:coin"
title="کارمزد طلایی"
:value="usePrice(finance?.fullPriceKarmozd ?? finance?.fullPriceKaermozd ?? 0, true, '')"
:help="copy.kpi.commission"
/>
<DashboardStatCard
tone="green"
icon="vuesax-linear:receipt-2"
title="تعداد سفارش"
:value="String(finance?.orderCount ?? 0)"
:help="copy.kpi.orderCount"
/>
<DashboardStatCard
tone="blue"
icon="vuesax-linear:empty-wallet"
title="تراز طلایی"
:value="formatGram(balance?.goldBalance)"
:help="copy.kpi.goldBalance"
/>
<DashboardStatCard
tone="rose"
icon="vuesax-linear:dollar-circle"
title="تراز ریالی"
:value="usePrice(balance?.cashBalance ?? 0, true, '')"
:help="copy.kpi.cashBalance"
/>
<DashboardStatCard
tone="violet"
icon="vuesax-linear:chart-2"
title="تراز مظنه"
:value="usePrice(balance?.mazanehBalance ?? 0, true, '')"
:help="copy.kpi.mazanehBalance"
/>
</section>
<!-- Date filter -->
<section
class="mt-2 rounded-xl border border-slate-200 bg-white px-3 py-2 dark:border-slate-600 dark:bg-slate-900"
>
<DashboardSectionHeader title="بازه گزارش" :help="copy.dateRange" />
<div class="mb-2 grid grid-cols-1 gap-2 sm:grid-cols-3">
<div
v-for="metric in dateRangeMetrics"
:key="metric.label"
class="rounded-lg bg-slate-50 px-2.5 py-2 dark:bg-slate-800/60"
>
<DashboardLabelWithHelp
:label="metric.label"
:help="metric.help"
size="xs"
text-class="text-[10px]"
label-class="text-slate-600 dark:text-slate-400"
/>
<p class="text-sm font-bold text-slate-900 dark:text-slate-50">{{ metric.value }}</p>
</div>
</div>
<div class="flex flex-wrap items-end gap-2">
<BaseDatePicker v-model:value="startDate" format="YYYY-MM-DD HH:mm:ss" class="!w-44" />
<span class="pb-1 text-xs text-slate-500 dark:text-slate-400">تا</span>
<BaseDatePicker v-model:value="endDate" format="YYYY-MM-DD HH:mm:ss" class="!w-44" />
<a-button type="primary" size="small" @click="applyDateRange">اعمال</a-button>
<a-button size="small" @click="refreshAll">بروزرسانی</a-button>
</div>
</section>
<!-- Charts row 1 -->
<section class="mt-2 grid grid-cols-1 gap-2 xl:grid-cols-2">
<div class="rounded-xl border border-slate-200 bg-white p-3 dark:border-slate-600 dark:bg-slate-900">
<DashboardSectionHeader title="ترکیب درآمد" :help="copy.charts.incomeMix" />
<apexchart
v-if="(finance?.fullPriceSood ?? 0) + (finance?.fullPriceKarmozd ?? 0) > 0"
type="donut"
height="220"
:options="financeChartOptions"
:series="financeChartOptions.series"
/>
<div
v-else
class="flex h-44 items-center justify-center text-xs text-slate-500 dark:text-slate-400"
>
در این بازه داده مالی ثبت نشده
</div>
</div>
<div class="rounded-xl border border-slate-200 bg-white p-3 dark:border-slate-600 dark:bg-slate-900">
<div class="flex items-start justify-between gap-2">
<DashboardSectionHeader
class="!mb-0 min-w-0 flex-1"
title="حجم خرید و فروش"
:help="copy.charts.buySellVolume"
/>
<a-segmented
v-model:value="flowRange"
size="small"
class="mt-0.5 shrink-0"
:options="[
{ label: '۷ روز', value: 'week' },
{ label: '۳۰ روز', value: 'month' },
]"
@change="loadFlow"
/>
</div>
<a-spin :spinning="flowLoading">
<apexchart
v-if="flowData.length"
type="bar"
height="220"
:options="flowChartOptions"
:series="flowChartOptions.series"
/>
<div
v-else
class="flex h-44 items-center justify-center text-xs text-slate-500 dark:text-slate-400"
>
داده‌ای برای نمودار نیست
</div>
</a-spin>
</div>
</section>
<!-- Price chart -->
<section
class="mt-2 rounded-xl border border-slate-200 bg-white p-3 dark:border-slate-600 dark:bg-slate-900"
>
<div class="flex flex-wrap items-start justify-between gap-2">
<DashboardSectionHeader
class="!mb-0 min-w-0 flex-1"
title="نمودار مظنه"
:help="copy.charts.priceChart"
/>
<a-segmented
v-model:value="priceRange"
size="small"
class="mt-0.5 shrink-0"
:options="[
{ label: '۲۴س', value: 'DAILY' },
{ label: 'هفته', value: 'WEEKLY' },
{ label: 'ماه', value: 'MONTHLY' },
]"
@change="switchPriceRange"
/>
</div>
<apexchart
v-if="priceSeries.length"
type="area"
height="200"
:options="priceChartOptions"
:series="priceChartOptions.series"
/>
<div
v-else
class="flex h-40 items-center justify-center text-xs text-slate-500 dark:text-slate-400"
>
داده نمودار قیمت در دسترس نیست
</div>
</section>
<!-- Orders -->
<section class="mt-2 grid grid-cols-1 gap-2 xl:grid-cols-3">
<div
class="rounded-xl border border-amber-200 bg-amber-50/40 p-3 dark:border-amber-800/50 dark:bg-amber-950/25 xl:col-span-1"
>
<div class="mb-2 flex items-start justify-between gap-2">
<DashboardSectionHeader
class="!mb-0 min-w-0 flex-1"
title="سفارشات باز"
:help="copy.orders.pending"
/>
<a-badge :count="pendingOrders.length" :overflow-count="99" />
</div>
<div v-if="pendingOrders.length" class="max-h-64 space-y-1.5 overflow-y-auto">
<div
v-for="order in pendingOrders"
:key="order.id"
class="rounded-lg border border-amber-200/80 bg-white p-2 dark:border-amber-800/40 dark:bg-slate-900"
>
<div class="flex items-center justify-between gap-2">
<span class="font-bold text-slate-900 dark:text-slate-100">#{{ order.id }}</span>
<span class="rounded-lg px-2 py-0.5 text-xs" :class="directionClass(order.direction)">
{{ directionLabel(order.direction) }}
</span>
</div>
<p class="mt-1 truncate text-sm text-slate-800 dark:text-slate-200">
{{ order.customer?.name ?? '—' }}
</p>
<div class="mt-2 grid grid-cols-2 gap-1 text-xs text-slate-700 dark:text-slate-300">
<span>مقدار: {{ formatGram(order.amount) }}</span>
<span>مظنه: {{ usePrice(order.price_original ?? 0, true, '') }}</span>
<span>قیمت مشتری: {{ usePrice(order.price ?? 0, true, '') }}</span>
<span class="font-medium text-emerald-700 dark:text-emerald-400">
سود: {{ usePrice(order.total_fee ?? 0, true, '') }}
</span>
</div>
</div>
</div>
<div v-else class="py-6 text-center text-xs text-amber-800/70 dark:text-amber-300/70">
سفارش بازی نیست
</div>
</div>
<div
class="rounded-xl border border-slate-200 bg-white p-3 dark:border-slate-600 dark:bg-slate-900 xl:col-span-2"
>
<div class="mb-2 flex items-start justify-between gap-2">
<DashboardSectionHeader
class="!mb-0 min-w-0 flex-1"
title="آخرین سفارشات"
:help="copy.orders.recent"
/>
<router-link :to="{ name: 'ordersMain' }">
<a-button type="link" class="!px-0">همه سفارشات</a-button>
</router-link>
</div>
<div class="overflow-x-auto">
<table class="w-full min-w-[52rem] text-sm">
<thead>
<tr class="border-b border-slate-200 text-xs text-slate-600 dark:border-slate-700 dark:text-slate-300">
<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>
</thead>
<tbody>
<tr
v-for="order in recentOrders"
:key="order.id"
class="border-b border-slate-100 text-slate-800 hover:bg-slate-50 dark:border-slate-800 dark:text-slate-200 dark:hover:bg-slate-800/50"
>
<td class="px-2 py-2.5 font-medium">#{{ order.id }}</td>
<td class="px-2 py-2.5">
<span class="rounded-md px-2 py-0.5 text-xs" :class="directionClass(order.direction)">
{{ directionLabel(order.direction) }}
</span>
</td>
<td class="max-w-[8rem] truncate px-2 py-2.5">{{ order.customer?.name ?? '—' }}</td>
<td class="px-2 py-2.5">{{ formatGram(order.amount) }}</td>
<td class="px-2 py-2.5">{{ usePrice(order.price_original ?? 0, true, '') }}</td>
<td class="px-2 py-2.5">{{ usePrice(order.price ?? 0, true, '') }}</td>
<td class="px-2 py-2.5 text-emerald-700 dark:text-emerald-400">
{{ usePrice(order.fee ?? 0, true, '') }}
</td>
<td class="px-2 py-2.5 font-semibold text-emerald-700 dark:text-emerald-400">
{{ usePrice(order.total_fee ?? 0, true, '') }}
</td>
<td class="px-2 py-2.5">
<span class="rounded-md px-2 py-0.5 text-xs" :class="stsClass(order.sts)">
{{ stsLabel(order) }}
</span>
</td>
<td class="whitespace-nowrap px-2 py-2.5 text-xs text-slate-500 dark:text-slate-400">
{{ order.created_at ? toJalali(order.created_at) : '—' }}
</td>
</tr>
</tbody>
</table>
</div>
</div>
</section>
<!-- Bottom row -->
<section class="mt-2 grid grid-cols-1 gap-2 lg:grid-cols-2">
<div class="rounded-xl border border-slate-200 bg-white p-3 dark:border-slate-600 dark:bg-slate-900">
<DashboardSectionHeader title="مشتریان" :help="copy.customers" />
<div class="grid grid-cols-2 gap-2">
<div
v-for="item in customerMetricItems"
:key="item.label"
class="rounded-lg px-2.5 py-2"
:class="item.bgClass"
>
<DashboardLabelWithHelp
:label="item.label"
:help="item.help"
size="xs"
text-class="text-[10px]"
:label-class="item.labelClass"
/>
<p class="text-lg font-bold" :class="item.valueClass">{{ item.value }}</p>
</div>
</div>
</div>
<div class="rounded-xl border border-slate-200 bg-white p-3 dark:border-slate-600 dark:bg-slate-900">
<DashboardSectionHeader title="موجودی کیف پول‌ها" :help="copy.wallet" />
<div class="space-y-1.5 text-sm text-slate-800 dark:text-slate-200">
<div
v-for="item in walletMetricItems"
:key="item.label"
class="flex items-center justify-between gap-2 rounded-lg px-2.5 py-2"
:class="item.bgClass"
>
<DashboardLabelWithHelp
:label="item.label"
:help="item.help"
size="sm"
label-class="text-slate-800 dark:text-slate-200"
/>
<span class="shrink-0 font-bold text-slate-900 dark:text-slate-50">{{ item.value }}</span>
</div>
</div>
</div>
</section>
<section
v-if="(master?.GT_products_report?.length ?? 0) > 0"
class="mt-2 rounded-xl border border-slate-200 bg-white p-3 dark:border-slate-600 dark:bg-slate-900"
>
<DashboardSectionHeader title="فعالیت محصولات" :help="copy.charts.productActivity" />
<apexchart type="bar" height="220" :options="productChartOptions" :series="productChartOptions.series" />
</section>
</a-spin>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import DashboardLabelWithHelp from '@/pages/dashboard/components/DashboardLabelWithHelp.vue'
import DashboardSectionHeader from '@/pages/dashboard/components/DashboardSectionHeader.vue'
import DashboardStatCard from '@/pages/dashboard/components/DashboardStatCard.vue'
import {
dashboardCharts,
dashboardCustomerMetrics,
dashboardCustomers,
dashboardDateMetrics,
dashboardDateRange,
dashboardGateway,
dashboardHero,
dashboardKpi,
dashboardOrders,
dashboardWallet,
dashboardWalletMetrics,
} from '@/pages/dashboard/dashboardSectionCopy'
import { useDashboardPage } from '@/pages/dashboard/composables/useDashboardPage'
const copy = {
hero: dashboardHero,
gateway: dashboardGateway,
kpi: dashboardKpi,
dateRange: dashboardDateRange,
charts: dashboardCharts,
orders: dashboardOrders,
customers: dashboardCustomers,
wallet: dashboardWallet,
}
const {
loading,
flowLoading,
startDate,
endDate,
master,
finance,
balance,
walletSums,
recentOrders,
pendingOrders,
customerStats,
priceRange,
flowRange,
flowData,
priceSeries,
refreshAll,
applyDateRange,
switchPriceRange,
loadFlow,
onToggleGateway,
formatGram,
directionLabel,
directionClass,
stsClass,
stsLabel,
financeChartOptions,
flowChartOptions,
priceChartOptions,
productChartOptions,
usePrice,
toJalali,
} = useDashboardPage()
const dateRangeMetrics = computed(() => [
{
label: 'واریز بانکی',
value: usePrice(balance.value?.income_money ?? 0),
help: dashboardDateMetrics.bankDeposit,
},
{
label: 'درآمد خالص',
value: usePrice(finance.value?.sumAll ?? 0),
help: dashboardDateMetrics.netIncome,
},
{
label: 'هدایا',
value: `${finance.value?.Gifts ?? '0'} گرم`,
help: dashboardDateMetrics.gifts,
},
])
const customerMetricItems = computed(() => [
{
label: 'کل مشتریان',
value: customerStats.value.total,
help: dashboardCustomerMetrics.total,
bgClass: 'bg-slate-50 dark:bg-slate-800/60',
labelClass: 'text-slate-600 dark:text-slate-400',
valueClass: 'text-slate-900 dark:text-slate-50',
},
{
label: 'جدید (۳ ماه)',
value: customerStats.value.newCount,
help: dashboardCustomerMetrics.new,
bgClass: 'bg-slate-50 dark:bg-slate-800/60',
labelClass: 'text-slate-600 dark:text-slate-400',
valueClass: 'text-slate-900 dark:text-slate-50',
},
{
label: 'فعال',
value: customerStats.value.active,
help: dashboardCustomerMetrics.active,
bgClass: 'bg-emerald-50 dark:bg-emerald-950/40',
labelClass: 'text-emerald-800 dark:text-emerald-300',
valueClass: 'text-emerald-800 dark:text-emerald-300',
},
{
label: 'بدون احراز',
value: customerStats.value.notVerified,
help: dashboardCustomerMetrics.notVerified,
bgClass: 'bg-rose-50 dark:bg-rose-950/40',
labelClass: 'text-rose-800 dark:text-rose-300',
valueClass: 'text-rose-800 dark:text-rose-300',
},
])
const walletMetricItems = computed(() => [
{
label: 'طلای مشتریان',
value: formatGram(walletSums.value?.gold),
help: dashboardWalletMetrics.gold,
bgClass: 'bg-amber-50 dark:bg-amber-950/35',
},
{
label: 'ریال مشتریان',
value: usePrice(walletSums.value?.cash ?? 0, true, ''),
help: dashboardWalletMetrics.cash,
bgClass: 'bg-sky-50 dark:bg-sky-950/35',
},
{
label: 'ارزش طلا به ریال',
value: usePrice(walletSums.value?.goldToCash ?? 0, true, ''),
help: dashboardWalletMetrics.goldToCash,
bgClass: 'bg-violet-50 dark:bg-violet-950/35',
},
])
</script>
<style scoped>
.dashboard-v2 :deep(.apexcharts-tooltip) {
direction: rtl;
}
.dashboard-v2 :deep(.dashboard-label-with-help .ant-tooltip-disabled-compatible-wrapper),
.dashboard-v2 :deep(.dashboard-label-with-help .dashboard-help-trigger) {
display: inline-flex !important;
align-items: center !important;
line-height: 0 !important;
vertical-align: middle !important;
}
</style>