532 lines
20 KiB
Vue
532 lines
20 KiB
Vue
<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>
|