cheng dashbord data and ui
This commit is contained in:
@@ -133,8 +133,11 @@ const EXACT: Record<string, string> = {
|
||||
'fashion/table': 'admin/misc/newsletter/list',
|
||||
'log/list/customer': 'admin/misc/log/customers',
|
||||
'dashboard/link': 'admin/misc/dashboard-links',
|
||||
// The two legacy dashboards are merged into one screen; both old names
|
||||
// keep working (aliased server-side) but the new dashboard calls this.
|
||||
'get-analytics': 'admin/dashboard/omde',
|
||||
'get-analytics-tak': 'admin/dashboard/tak',
|
||||
'dashboard/overview': 'admin/dashboard/overview',
|
||||
'brand/create': 'admin/misc/brands',
|
||||
// NOTE 'ciny' itself is already mapped in the categories block below.
|
||||
'ciny/create': 'admin/categories/collections',
|
||||
|
||||
@@ -96,7 +96,10 @@
|
||||
// most visible difference across the 54 card sites in the port.
|
||||
body #app .ant-card {
|
||||
border-radius: 0.5rem;
|
||||
box-shadow: 0 4px 25px 0 rgba(0, 0, 0, 0.1);
|
||||
// Softened twice per request: the original 25px/10% shadow read as heavy
|
||||
// on every card in the panel (54 sites); this is now barely-there —
|
||||
// a faint depth cue rather than a visible shadow.
|
||||
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.03);
|
||||
}
|
||||
|
||||
// ── Full-height list pages ──────────────────────────────────────────
|
||||
|
||||
Vendored
+10
@@ -5,3 +5,13 @@ declare module '*.vue' {
|
||||
const component: DefineComponent<Record<string, unknown>, Record<string, unknown>, unknown>;
|
||||
export default component;
|
||||
}
|
||||
|
||||
// vue3-apexcharts registers <apexchart> globally via app.use() (main.ts); this
|
||||
// lets template type-checking (vue-tsc) resolve the tag instead of erroring
|
||||
// on an unknown component.
|
||||
import type { VueApexChartsComponentProps } from 'vue3-apexcharts';
|
||||
declare module '@vue/runtime-core' {
|
||||
export interface GlobalComponents {
|
||||
apexchart: import('vue').DefineComponent<VueApexChartsComponentProps>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,29 +34,16 @@ export interface NavMenuNode {
|
||||
|
||||
export const adminMenu: NavMenuNode[] = [
|
||||
{
|
||||
url: null,
|
||||
// "آمار عمده فروشی" and "آمار تک فروشی" were two submenu entries behind
|
||||
// two rids, pointing at two near-duplicate dashboards. Merged into one
|
||||
// link behind one rid (dashboard.view) — see Dashboard.vue.
|
||||
url: '/dashboard/ecommerce',
|
||||
name: 'پیشخوان',
|
||||
// tag: '2',
|
||||
tagColor: 'warning',
|
||||
icon: 'HomeIcon',
|
||||
i18n: 'پیشخوان',
|
||||
permissionId:"dashboard",
|
||||
submenu: [
|
||||
{
|
||||
url: '/dashboard/ecommerce',
|
||||
name: 'eCommerce',
|
||||
slug: 'dashboard-ecommerce',
|
||||
i18n: 'آمار عمده فروشی',
|
||||
permissionId:"dashboard.omde"
|
||||
},
|
||||
{
|
||||
url: '/dashboard/analytics',
|
||||
name: 'eCommerce',
|
||||
slug: 'dashboard-ecommerce',
|
||||
i18n: 'آمار تک فروشی',
|
||||
permissionId:"dashboard.tak"
|
||||
},
|
||||
]
|
||||
permissionId:"dashboard.view",
|
||||
},
|
||||
{
|
||||
header: 'Apps',
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import { createApp } from 'vue';
|
||||
import { createPinia } from 'pinia';
|
||||
import Antd from 'ant-design-vue';
|
||||
import VueApexCharts from 'vue3-apexcharts';
|
||||
|
||||
import App from './App.vue';
|
||||
import router from './router';
|
||||
@@ -16,5 +17,6 @@ const app = createApp(App);
|
||||
app.use(createPinia());
|
||||
app.use(Antd);
|
||||
app.use(router);
|
||||
app.use(VueApexCharts);
|
||||
|
||||
app.mount('#app');
|
||||
|
||||
@@ -29,12 +29,16 @@ export const adminRoute: RouteRecordRaw[] = [
|
||||
redirect: '/my-data'
|
||||
},
|
||||
{
|
||||
// Legacy had two dashboards ("آمار تک فروشی" at /dashboard/analytics and
|
||||
// "آمار عمده فروشی" at /dashboard/ecommerce) behind two rids. Merged into
|
||||
// one screen behind one rid (dashboard.view) — see Dashboard.vue. Both
|
||||
// old paths still resolve so any bookmarked/linked URL keeps working.
|
||||
path: '/dashboard/analytics',
|
||||
name: 'dashboard-analytics',
|
||||
component: () => import('../views/DashboardAnalytics.vue'),
|
||||
component: () => import('../views/Dashboard.vue'),
|
||||
meta: {
|
||||
rule: 'editor',
|
||||
role: 'dashboard.tak'
|
||||
role: 'dashboard.view'
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -49,10 +53,10 @@ export const adminRoute: RouteRecordRaw[] = [
|
||||
{
|
||||
path: '/dashboard/ecommerce',
|
||||
name: 'dashboard-ecommerce',
|
||||
component: () => import('../views/DashboardECommerce.vue'),
|
||||
component: () => import('../views/Dashboard.vue'),
|
||||
meta: {
|
||||
rule: 'admin',
|
||||
role: 'dashboard.omde'
|
||||
role: 'dashboard.view'
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -22,7 +22,9 @@ export const branchRoute: RouteRecordRaw[] = [
|
||||
component: () => import('../views/branch/dashboard.vue'),
|
||||
meta: {
|
||||
rule: 'admin',
|
||||
role: 'dashboard.omde'
|
||||
// was 'dashboard.omde' — merged into the single 'dashboard.view' rid
|
||||
// when the two admin dashboards became one screen.
|
||||
role: 'dashboard.view'
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -0,0 +1,319 @@
|
||||
<template>
|
||||
<!--
|
||||
Replaces legacy DashboardECommerce.vue ("آمار عمده فروشی") and
|
||||
DashboardAnalytics.vue ("آمار تک فروشی") — two menu entries, two rids,
|
||||
two near-identical screens (the retail one rendered almost nothing real;
|
||||
see admin-dashboard.service.ts's header comment). Merged into one
|
||||
dashboard behind one rid (dashboard.view) and one endpoint
|
||||
(GET dashboard/overview), which returns the union of both legacy
|
||||
payloads unchanged plus the new chart aggregates used below.
|
||||
-->
|
||||
<a-spin :spinning="loading">
|
||||
<!-- ── KPI row: every stat the two legacy dashboards showed, preserved ── -->
|
||||
<div class="grid-4">
|
||||
<StatisticsCardLine
|
||||
icon="UsersIcon"
|
||||
color="warning"
|
||||
:statistic="`${data.customerCount ?? 0} نفر`"
|
||||
statistic-title="مشتریان جدید (۳۰ روز گذشته)"
|
||||
/>
|
||||
<StatisticsCardLine
|
||||
icon="ShoppingBagIcon"
|
||||
color="primary"
|
||||
:statistic="`${data.allgram ?? 0} گرم`"
|
||||
statistic-title="طلای فروش رفته (۳۰ روز گذشته)"
|
||||
/>
|
||||
<StatisticsCardLine
|
||||
icon="ShoppingBagIcon"
|
||||
color="success"
|
||||
:statistic="`${data.ordercount ?? 0} سفارش`"
|
||||
statistic-title="تعداد سفارشات (۳۰ روز گذشته)"
|
||||
/>
|
||||
<StatisticsCardLine
|
||||
icon="DollarSignIcon"
|
||||
color="danger"
|
||||
:statistic="goldPriceLabel"
|
||||
statistic-title="نرخ هر گرم طلای ۱۸ عیار"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="grid-2 mt-4">
|
||||
<StatisticsCardLine
|
||||
icon="BarChartIcon"
|
||||
color="primary"
|
||||
:statistic="`${data.sumLastOrder ?? 0} سفارش`"
|
||||
statistic-title="تعداد سفارشات تحویلشده (۳۰ روز گذشته)"
|
||||
/>
|
||||
<StatisticsCardLine
|
||||
icon="CreditCardIcon"
|
||||
color="warning"
|
||||
:statistic="`${totalDebtLabel} گرم`"
|
||||
statistic-title="مجموع بدهی طلای مشتریان"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- ── روند سفارشها: 30-day line chart, تک vs عمده ── -->
|
||||
<a-card title="روند سفارشها (۳۰ روز گذشته)" class="mt-4">
|
||||
<apexchart
|
||||
type="area"
|
||||
height="300"
|
||||
:options="trendChartOptions"
|
||||
:series="trendSeries"
|
||||
/>
|
||||
</a-card>
|
||||
|
||||
<div class="grid-2 mt-4">
|
||||
<a-card title="وضعیت سفارشات (۳۰ روز گذشته)">
|
||||
<apexchart type="donut" height="300" :options="statusChartOptions" :series="statusSeries" />
|
||||
</a-card>
|
||||
|
||||
<a-card title="تفکیک عمده / تک (۳۰ روز گذشته)">
|
||||
<apexchart type="donut" height="300" :options="channelChartOptions" :series="channelSeries" />
|
||||
</a-card>
|
||||
</div>
|
||||
|
||||
<div class="grid-2 mt-4">
|
||||
<a-card title="عملکرد کارشناسان فروش">
|
||||
<apexchart
|
||||
v-if="agentChartSeries[0]?.data.length"
|
||||
type="bar"
|
||||
height="280"
|
||||
:options="agentChartOptions"
|
||||
:series="agentChartSeries"
|
||||
/>
|
||||
<a-table
|
||||
class="mt-3"
|
||||
size="small"
|
||||
:data-source="data.reagent"
|
||||
:columns="agentColumns"
|
||||
:pagination="false"
|
||||
row-key="id"
|
||||
/>
|
||||
</a-card>
|
||||
|
||||
<a-card title="محصولات پرفروش (۳۰ روز گذشته)">
|
||||
<apexchart
|
||||
v-if="productChartSeries[0]?.data.length"
|
||||
type="bar"
|
||||
height="280"
|
||||
:options="productChartOptions"
|
||||
:series="productChartSeries"
|
||||
/>
|
||||
<a-empty v-else description="دادهای برای نمایش وجود ندارد" />
|
||||
</a-card>
|
||||
</div>
|
||||
|
||||
<a-card title="میزان فروش" class="mt-4">
|
||||
<a-descriptions bordered size="small" :column="3">
|
||||
<a-descriptions-item label="تعداد سفارش (۳۰ روز)">
|
||||
{{ data.sumLastOrder ?? 0 }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="مجموع وزن (۳۰ روز)">
|
||||
{{ data.sumLastOrderMonth ?? 0 }} گرم
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="میانگین هر سفارش">
|
||||
{{ avgWeightPerOrder }} گرم
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</a-card>
|
||||
|
||||
<a-card title="سفارشات هفته جاری" class="mt-4">
|
||||
<a-table
|
||||
size="small"
|
||||
:data-source="data.lastOrder"
|
||||
:columns="orderColumns"
|
||||
:pagination="{ pageSize: 10 }"
|
||||
row-key="id"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.dataIndex === 'sts_str'">
|
||||
<a-tag :color="tagColor(record.statusColor)">{{ record.sts_str }}</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.dataIndex === 'caj'">
|
||||
{{ jdate(record.caj) }}
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
</a-spin>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, reactive, ref } from 'vue';
|
||||
import StatisticsCardLine from '../components/statistics-cards/StatisticsCardLine.vue';
|
||||
import { jdate, price } from '../utils/filters';
|
||||
import api from '../api/client';
|
||||
|
||||
const loading = ref(false);
|
||||
|
||||
interface OrdersTrendPoint {
|
||||
date: string;
|
||||
total: number;
|
||||
retail: number;
|
||||
wholesale: number;
|
||||
}
|
||||
interface StatusBreakdownRow {
|
||||
status: string;
|
||||
label: string;
|
||||
count: number;
|
||||
}
|
||||
interface ChannelBreakdownRow {
|
||||
channel: string;
|
||||
label: string;
|
||||
count: number;
|
||||
grams: number;
|
||||
}
|
||||
interface TopProductRow {
|
||||
title: string;
|
||||
weight: number;
|
||||
qty: number;
|
||||
}
|
||||
|
||||
const data = reactive<Record<string, any>>({
|
||||
customerCount: 0,
|
||||
allgram: 0,
|
||||
ordercount: 0,
|
||||
reagent: [],
|
||||
lastOrder: [],
|
||||
sumLastOrder: 0,
|
||||
sumLastOrderMonth: 0,
|
||||
ordersTrend: [] as OrdersTrendPoint[],
|
||||
statusBreakdown: [] as StatusBreakdownRow[],
|
||||
channelBreakdown: [] as ChannelBreakdownRow[],
|
||||
topProducts: [] as TopProductRow[],
|
||||
goldPricePerGram: null as number | null,
|
||||
totalCustomerDebtGrams: 0,
|
||||
});
|
||||
|
||||
// ── colours matching the rest of the admin panel (StatisticsCardLine's own
|
||||
// icon-color classes and ant's card chrome) ────────────────────────────────
|
||||
const COLOR_PRIMARY = '#1c2d56';
|
||||
const COLOR_SUCCESS = '#28c76f';
|
||||
const COLOR_WARNING = '#ff9f43';
|
||||
const COLOR_DANGER = '#ea5455';
|
||||
|
||||
const agentColumns = [
|
||||
{ title: 'کارشناس فروش', dataIndex: 'name' },
|
||||
{ title: 'میزان فروش (گرم)', dataIndex: 'grams_sell' },
|
||||
{ title: 'تعداد سفارش', dataIndex: 'order_count' },
|
||||
];
|
||||
|
||||
const orderColumns = [
|
||||
{ title: 'شماره سفارش', dataIndex: 'name' },
|
||||
{ title: 'مشتری', dataIndex: 'customer' },
|
||||
{ title: 'وزن', dataIndex: 'w' },
|
||||
{ title: 'وضعیت', dataIndex: 'sts_str' },
|
||||
{ title: 'تاریخ', dataIndex: 'caj' },
|
||||
];
|
||||
|
||||
function tagColor(c: string) {
|
||||
return { success: 'green', warning: 'orange', danger: 'red', primary: 'blue' }[c] ?? '';
|
||||
}
|
||||
|
||||
const goldPriceLabel = computed(() =>
|
||||
data.goldPricePerGram ? `${price(Math.round(Number(data.goldPricePerGram)))} تومان` : '—',
|
||||
);
|
||||
|
||||
const totalDebtLabel = computed(() => price(Number(data.totalCustomerDebtGrams ?? 0).toFixed(2)));
|
||||
|
||||
const avgWeightPerOrder = computed(() => {
|
||||
const count = Number(data.sumLastOrder ?? 0);
|
||||
const weight = Number(data.sumLastOrderMonth ?? 0);
|
||||
return count > 0 ? (weight / count).toFixed(2) : '0';
|
||||
});
|
||||
|
||||
// ── روند سفارشها: area chart, تک/عمده بهصورت جدا ──────────────────────
|
||||
const trendSeries = computed(() => {
|
||||
const trend: OrdersTrendPoint[] = data.ordersTrend ?? [];
|
||||
return [
|
||||
{ name: 'تک فروشی', data: trend.map((d) => d.retail) },
|
||||
{ name: 'عمده فروشی', data: trend.map((d) => d.wholesale) },
|
||||
];
|
||||
});
|
||||
const trendChartOptions = computed(() => ({
|
||||
chart: { toolbar: { show: false }, fontFamily: 'inherit' },
|
||||
colors: [COLOR_PRIMARY, COLOR_SUCCESS],
|
||||
dataLabels: { enabled: false },
|
||||
stroke: { curve: 'smooth', width: 2 },
|
||||
fill: { type: 'gradient', gradient: { opacityFrom: 0.35, opacityTo: 0.05 } },
|
||||
legend: { position: 'top', horizontalAlign: 'right' },
|
||||
xaxis: {
|
||||
categories: (data.ordersTrend ?? []).map((d: OrdersTrendPoint) => jdate(d.date)),
|
||||
labels: { rotate: 0 },
|
||||
},
|
||||
tooltip: { shared: true },
|
||||
}));
|
||||
|
||||
// ── وضعیت سفارشات: donut ────────────────────────────────────────────────
|
||||
const statusSeries = computed(() => (data.statusBreakdown ?? []).map((s: StatusBreakdownRow) => s.count));
|
||||
const statusChartOptions = computed(() => ({
|
||||
chart: { fontFamily: 'inherit' },
|
||||
labels: (data.statusBreakdown ?? []).map((s: StatusBreakdownRow) => s.label),
|
||||
colors: [COLOR_WARNING, COLOR_PRIMARY, COLOR_PRIMARY, COLOR_WARNING, COLOR_WARNING, COLOR_SUCCESS, COLOR_SUCCESS, COLOR_DANGER],
|
||||
legend: { position: 'bottom' },
|
||||
dataLabels: { enabled: true },
|
||||
}));
|
||||
|
||||
// ── تفکیک عمده/تک: donut ────────────────────────────────────────────────
|
||||
const channelSeries = computed(() => (data.channelBreakdown ?? []).map((c: ChannelBreakdownRow) => c.count));
|
||||
const channelChartOptions = computed(() => ({
|
||||
chart: { fontFamily: 'inherit' },
|
||||
labels: (data.channelBreakdown ?? []).map((c: ChannelBreakdownRow) => c.label),
|
||||
colors: [COLOR_PRIMARY, COLOR_SUCCESS],
|
||||
legend: { position: 'bottom' },
|
||||
dataLabels: { enabled: true },
|
||||
}));
|
||||
|
||||
// ── عملکرد کارشناسان: horizontal bar ────────────────────────────────────
|
||||
const agentChartSeries = computed(() => [
|
||||
{ name: 'گرم فروختهشده', data: (data.reagent ?? []).map((r: any) => Number(r.grams_sell ?? 0)) },
|
||||
]);
|
||||
const agentChartOptions = computed(() => ({
|
||||
chart: { toolbar: { show: false }, fontFamily: 'inherit' },
|
||||
plotOptions: { bar: { horizontal: true, borderRadius: 4 } },
|
||||
colors: [COLOR_PRIMARY],
|
||||
dataLabels: { enabled: true },
|
||||
xaxis: { categories: (data.reagent ?? []).map((r: any) => r.name) },
|
||||
}));
|
||||
|
||||
// ── محصولات پرفروش: bar ─────────────────────────────────────────────────
|
||||
const productChartSeries = computed(() => [
|
||||
{ name: 'وزن فروختهشده (گرم)', data: (data.topProducts ?? []).map((p: TopProductRow) => Number(p.weight ?? 0)) },
|
||||
]);
|
||||
const productChartOptions = computed(() => ({
|
||||
chart: { toolbar: { show: false }, fontFamily: 'inherit' },
|
||||
plotOptions: { bar: { borderRadius: 4, columnWidth: '55%' } },
|
||||
colors: [COLOR_SUCCESS],
|
||||
dataLabels: { enabled: false },
|
||||
xaxis: { categories: (data.topProducts ?? []).map((p: TopProductRow) => p.title) },
|
||||
}));
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
loading.value = true;
|
||||
const { data: res } = await api.get('dashboard/overview');
|
||||
Object.assign(data, res);
|
||||
// legacy sorted both lists client-side after the fetch
|
||||
data.reagent = [...(data.reagent ?? [])].sort(
|
||||
(a: any, b: any) => Number(b.grams_sell) - Number(a.grams_sell),
|
||||
);
|
||||
data.lastOrder = [...(data.lastOrder ?? [])].sort(
|
||||
(a: any, b: any) => Number(b.w) - Number(a.w),
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* .grid-2/.grid-3/.grid-4 come from the global utilities.less (dedup) */
|
||||
.mt-4 {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
.mt-3 {
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
</style>
|
||||
@@ -1,64 +0,0 @@
|
||||
<template>
|
||||
<!--
|
||||
Ported from legacy henza_admin/src/views/DashboardAnalytics.vue — the retail
|
||||
dashboard ("آمار تک فروشی").
|
||||
|
||||
Legacy's file is 342 lines but reads only ONE field from the server:
|
||||
`model.lastOrder`. Everything else on the page is Vuexy demo data defined in
|
||||
the component itself (analyticsData.js series, radial bars, order-type
|
||||
colours) and renders nothing real — so only the live table is ported.
|
||||
-->
|
||||
<a-spin :spinning="loading">
|
||||
<a-card title="سفارشات هفته جاری">
|
||||
<a-table
|
||||
size="small"
|
||||
:data-source="model.lastOrder"
|
||||
:columns="columns"
|
||||
:pagination="{ pageSize: 15 }"
|
||||
row-key="id"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.dataIndex === 'sts_str'">
|
||||
<a-tag :color="tagColor(record.statusColor)">{{ record.sts_str }}</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.dataIndex === 'caj'">
|
||||
{{ jdate(record.caj) }}
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
</a-spin>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, reactive, ref } from 'vue';
|
||||
import { jdate } from '../utils/filters';
|
||||
import api from '../api/client';
|
||||
|
||||
const loading = ref(false);
|
||||
const model = reactive<Record<string, any>>({ lastOrder: [] });
|
||||
|
||||
const columns = [
|
||||
{ title: 'شماره سفارش', dataIndex: 'name' },
|
||||
{ title: 'مشتری', dataIndex: 'customer' },
|
||||
{ title: 'وزن', dataIndex: 'w' },
|
||||
{ title: 'وضعیت', dataIndex: 'sts_str' },
|
||||
{ title: 'تاریخ', dataIndex: 'caj' },
|
||||
];
|
||||
|
||||
function tagColor(c: string) {
|
||||
return { success: 'green', warning: 'orange', danger: 'red', primary: 'blue' }[c] ?? '';
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
loading.value = true;
|
||||
const { data } = await api.get('get-analytics-tak');
|
||||
Object.assign(model, data);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@@ -1,133 +0,0 @@
|
||||
<template>
|
||||
<!--
|
||||
Ported from legacy henza_admin/src/views/DashboardECommerce.vue — the
|
||||
wholesale dashboard ("آمار عمده فروشی").
|
||||
|
||||
Legacy's file is a Vuexy template whose English demo cards are all COMMENTED
|
||||
OUT (Congratulations, Support Tracker, Product Orders, Sales Stats, Activity
|
||||
Timeline — verified line by line). The live content is the three 30-day stat
|
||||
cards plus three Farsi tables, and that is what is ported.
|
||||
-->
|
||||
<a-spin :spinning="loading">
|
||||
<div class="grid-3">
|
||||
<StatisticsCardLine
|
||||
icon="UsersIcon"
|
||||
color="warning"
|
||||
:statistic="`${omdeData.customerCount} نفر `"
|
||||
statistic-title="تعداد مشتریان ثبت نامی در 30 روز گذشته"
|
||||
/>
|
||||
<StatisticsCardLine
|
||||
icon="ShoppingBagIcon"
|
||||
:statistic="`${omdeData.allgram} گرم `"
|
||||
statistic-title="طلای فروش رفته در 30 روز گذشته"
|
||||
/>
|
||||
<StatisticsCardLine
|
||||
icon="ShoppingBagIcon"
|
||||
color="warning"
|
||||
:statistic="`${omdeData.ordercount} سفارش `"
|
||||
statistic-title="تعداد سفارشات 30 روز گذشته"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="grid-2 mt-4">
|
||||
<a-card title="عملکرد کارشناسان فروش">
|
||||
<a-table
|
||||
size="small"
|
||||
:data-source="omdeData.reagent"
|
||||
:columns="agentColumns"
|
||||
:pagination="false"
|
||||
row-key="id"
|
||||
/>
|
||||
</a-card>
|
||||
|
||||
<a-card title="میزان فروش">
|
||||
<a-descriptions bordered size="small" :column="1">
|
||||
<a-descriptions-item label="تعداد سفارش (۳۰ روز)">
|
||||
{{ omdeData.sumLastOrder }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="مجموع وزن (۳۰ روز)">
|
||||
{{ omdeData.sumLastOrderMonth }} گرم
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</a-card>
|
||||
</div>
|
||||
|
||||
<a-card title="سفارشات هفته جاری" class="mt-4">
|
||||
<a-table
|
||||
size="small"
|
||||
:data-source="omdeData.lastOrder"
|
||||
:columns="orderColumns"
|
||||
:pagination="{ pageSize: 10 }"
|
||||
row-key="id"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.dataIndex === 'sts_str'">
|
||||
<a-tag :color="tagColor(record.statusColor)">{{ record.sts_str }}</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.dataIndex === 'caj'">
|
||||
{{ jdate(record.caj) }}
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
</a-spin>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, reactive, ref } from 'vue';
|
||||
import StatisticsCardLine from '../components/statistics-cards/StatisticsCardLine.vue';
|
||||
import { jdate } from '../utils/filters';
|
||||
import api from '../api/client';
|
||||
|
||||
const loading = ref(false);
|
||||
|
||||
const omdeData = reactive<Record<string, any>>({
|
||||
customerCount: 0,
|
||||
allgram: 0,
|
||||
ordercount: 0,
|
||||
reagent: [],
|
||||
lastOrder: [],
|
||||
sumLastOrder: 0,
|
||||
sumLastOrderMonth: 0,
|
||||
});
|
||||
|
||||
const agentColumns = [
|
||||
{ title: 'کارشناس فروش', dataIndex: 'name' },
|
||||
{ title: 'میزان فروش (گرم)', dataIndex: 'grams_sell' },
|
||||
{ title: 'تعداد سفارش', dataIndex: 'order_count' },
|
||||
];
|
||||
|
||||
const orderColumns = [
|
||||
{ title: 'شماره سفارش', dataIndex: 'name' },
|
||||
{ title: 'مشتری', dataIndex: 'customer' },
|
||||
{ title: 'وزن', dataIndex: 'w' },
|
||||
{ title: 'وضعیت', dataIndex: 'sts_str' },
|
||||
{ title: 'تاریخ', dataIndex: 'caj' },
|
||||
];
|
||||
|
||||
function tagColor(c: string) {
|
||||
return { success: 'green', warning: 'orange', danger: 'red', primary: 'blue' }[c] ?? '';
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
loading.value = true;
|
||||
const { data } = await api.get('get-analytics');
|
||||
Object.assign(omdeData, data);
|
||||
// legacy sorts both lists client-side after the fetch
|
||||
omdeData.reagent.sort((a: any, b: any) => Number(b.grams_sell) - Number(a.grams_sell));
|
||||
omdeData.lastOrder.sort((a: any, b: any) => Number(b.w) - Number(a.w));
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* .grid-2/.grid-3 now come from the global utilities.less (dedup) */
|
||||
.mt-4 {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
</style>
|
||||
@@ -22,18 +22,11 @@ export interface PermissionNode {
|
||||
|
||||
export const treeData: PermissionNode[] = [
|
||||
{
|
||||
// "آمار عمده فروشی" (dashboard.omde) and "آمار تک فروشی" (dashboard.tak)
|
||||
// were merged into one dashboard behind one rid when the two menu
|
||||
// entries became one — see navMenuItems.ts / Dashboard.vue.
|
||||
"title": "پیشخوان",
|
||||
"key": "dashboard",
|
||||
"children": [
|
||||
{
|
||||
"title": "آمار عمده فروشی",
|
||||
"key": "dashboard.omde"
|
||||
},
|
||||
{
|
||||
"title": "آمار تک فروشی",
|
||||
"key": "dashboard.tak"
|
||||
}
|
||||
]
|
||||
"key": "dashboard.view"
|
||||
},
|
||||
{
|
||||
"title": "اشخاص",
|
||||
|
||||
Reference in New Issue
Block a user