feat: show retail dashboard figures to the super admin only
پنل: بخشهای «تک فروشی» داشبورد (کارت، نمودار روند و تفکیک عمده/تک) فقط برای مدیر اصلی نمایش داده میشود. Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Haiku 5.5
parent
0520aaf1b1
commit
640a42da05
@@ -80,12 +80,12 @@
|
|||||||
<div class="panel__head">
|
<div class="panel__head">
|
||||||
<div>
|
<div>
|
||||||
<h3 class="panel__title">روند سفارشها</h3>
|
<h3 class="panel__title">روند سفارشها</h3>
|
||||||
<p class="panel__sub">۳۰ روز گذشته، تفکیک تک و عمده</p>
|
<p class="panel__sub">{{ isSuper ? '۳۰ روز گذشته، تفکیک تک و عمده' : '۳۰ روز گذشته' }}</p>
|
||||||
</div>
|
</div>
|
||||||
<a-segmented v-model:value="trendType" :options="trendTypeOptions" size="small" />
|
<a-segmented v-model:value="trendType" :options="trendTypeOptions" size="small" />
|
||||||
</div>
|
</div>
|
||||||
<div class="trend-totals">
|
<div class="trend-totals">
|
||||||
<div class="trend-totals__item">
|
<div v-if="isSuper" class="trend-totals__item">
|
||||||
<span class="dot" :style="{ background: COLOR_PRIMARY }"></span>
|
<span class="dot" :style="{ background: COLOR_PRIMARY }"></span>
|
||||||
<span>تک فروشی</span>
|
<span>تک فروشی</span>
|
||||||
<b>{{ fmt(retailTotal) }}</b>
|
<b>{{ fmt(retailTotal) }}</b>
|
||||||
@@ -105,7 +105,7 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="panel">
|
<div v-if="isSuper" class="panel">
|
||||||
<div class="panel__head">
|
<div class="panel__head">
|
||||||
<div>
|
<div>
|
||||||
<h3 class="panel__title">تفکیک عمده / تک</h3>
|
<h3 class="panel__title">تفکیک عمده / تک</h3>
|
||||||
@@ -295,10 +295,14 @@ import {
|
|||||||
} from '@ant-design/icons-vue';
|
} from '@ant-design/icons-vue';
|
||||||
import { jdate, price } from '../utils/filters';
|
import { jdate, price } from '../utils/filters';
|
||||||
import { useLayoutStore } from '../stores/layout';
|
import { useLayoutStore } from '../stores/layout';
|
||||||
|
import { useUsersStore } from '../stores/users';
|
||||||
import api from '../api/client';
|
import api from '../api/client';
|
||||||
|
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const layout = useLayoutStore();
|
const layout = useLayoutStore();
|
||||||
|
// Retail («تک فروشی») figures are for the super admin only; the API also omits them.
|
||||||
|
const users = useUsersStore();
|
||||||
|
const isSuper = computed(() => users.isSuperAdmin);
|
||||||
|
|
||||||
const chartHeight = computed(() => (layout.isMobile ? 240 : 300));
|
const chartHeight = computed(() => (layout.isMobile ? 240 : 300));
|
||||||
const donutHeight = computed(() => (layout.isMobile ? 220 : 240));
|
const donutHeight = computed(() => (layout.isMobile ? 220 : 240));
|
||||||
@@ -419,14 +423,13 @@ const wholesaleTotal = computed(() =>
|
|||||||
);
|
);
|
||||||
const trendSeries = computed(() => {
|
const trendSeries = computed(() => {
|
||||||
const trend: OrdersTrendPoint[] = data.ordersTrend ?? [];
|
const trend: OrdersTrendPoint[] = data.ordersTrend ?? [];
|
||||||
return [
|
const wholesale = { name: 'عمده فروشی', data: trend.map((d) => d.wholesale) };
|
||||||
{ name: 'تک فروشی', data: trend.map((d) => d.retail) },
|
if (!isSuper.value) return [wholesale];
|
||||||
{ name: 'عمده فروشی', data: trend.map((d) => d.wholesale) },
|
return [{ name: 'تک فروشی', data: trend.map((d) => d.retail) }, wholesale];
|
||||||
];
|
|
||||||
});
|
});
|
||||||
const trendChartOptions = computed(() => ({
|
const trendChartOptions = computed(() => ({
|
||||||
chart: { ...BASE_CHART, stacked: trendType.value === 'bar' },
|
chart: { ...BASE_CHART, stacked: trendType.value === 'bar' },
|
||||||
colors: [COLOR_PRIMARY, COLOR_SUCCESS],
|
colors: isSuper.value ? [COLOR_PRIMARY, COLOR_SUCCESS] : [COLOR_SUCCESS],
|
||||||
dataLabels: { enabled: false },
|
dataLabels: { enabled: false },
|
||||||
stroke: trendType.value === 'area' ? { curve: 'smooth', width: 2.5 } : { width: 0 },
|
stroke: trendType.value === 'area' ? { curve: 'smooth', width: 2.5 } : { width: 0 },
|
||||||
fill:
|
fill:
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { Controller, Get, Query } from '@nestjs/common';
|
import { Controller, Get, Query } from '@nestjs/common';
|
||||||
import { AdminDashboardService } from './admin-dashboard.service';
|
import { AdminDashboardService } from './admin-dashboard.service';
|
||||||
import { RequirePermission } from '../auth/guards';
|
import { CurrentAdmin, RequirePermission } from '../auth/guards';
|
||||||
|
import type { AuthenticatedAdmin } from '../auth/jwt.strategy';
|
||||||
|
import { isSuperAdmin } from '../auth/super-admin';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Legacy shipped two dashboards behind two rids:
|
* Legacy shipped two dashboards behind two rids:
|
||||||
@@ -18,8 +20,8 @@ export class AdminDashboardController {
|
|||||||
|
|
||||||
@Get('overview')
|
@Get('overview')
|
||||||
@RequirePermission('dashboard.view')
|
@RequirePermission('dashboard.view')
|
||||||
overview() {
|
overview(@CurrentAdmin() admin: AuthenticatedAdmin) {
|
||||||
return this.dashboard.overview();
|
return this.dashboard.overview(isSuperAdmin(admin.mobile));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @deprecated superseded by GET overview; kept as an alias. */
|
/** @deprecated superseded by GET overview; kept as an alias. */
|
||||||
|
|||||||
@@ -37,8 +37,12 @@ export class AdminDashboardService {
|
|||||||
status: { in: [OrderStatus.DELIVERED_TO_AGENT, OrderStatus.DELIVERED_TO_CUSTOMER] },
|
status: { in: [OrderStatus.DELIVERED_TO_AGENT, OrderStatus.DELIVERED_TO_CUSTOMER] },
|
||||||
};
|
};
|
||||||
|
|
||||||
/** The single merged dashboard endpoint (`GET admin/dashboard/overview`). */
|
/**
|
||||||
async overview() {
|
* The single merged dashboard endpoint (`GET admin/dashboard/overview`).
|
||||||
|
* Retail («تک فروشی») figures are only built for the super admin; for anyone
|
||||||
|
* else they never leave the server.
|
||||||
|
*/
|
||||||
|
async overview(viewerIsSuperAdmin = false) {
|
||||||
const since30 = this.daysAgo(30);
|
const since30 = this.daysAgo(30);
|
||||||
const since7 = this.daysAgo(7);
|
const since7 = this.daysAgo(7);
|
||||||
|
|
||||||
@@ -111,7 +115,11 @@ export class AdminDashboardService {
|
|||||||
if (o.channel === 'WHOLESALE') bucket.wholesale += 1;
|
if (o.channel === 'WHOLESALE') bucket.wholesale += 1;
|
||||||
else bucket.retail += 1;
|
else bucket.retail += 1;
|
||||||
}
|
}
|
||||||
const ordersTrend = [...trendByDay.entries()].map(([date, v]) => ({ date, ...v }));
|
// Non-super admins get wholesale only: their `total` is the wholesale count
|
||||||
|
// and there is no `retail` field, so the retail number cannot be derived.
|
||||||
|
const ordersTrend = [...trendByDay.entries()].map(([date, v]) =>
|
||||||
|
viewerIsSuperAdmin ? { date, ...v } : { date, total: v.wholesale, wholesale: v.wholesale },
|
||||||
|
);
|
||||||
|
|
||||||
const statusLabels: Record<OrderStatus, string> = {
|
const statusLabels: Record<OrderStatus, string> = {
|
||||||
SUSPENDED: 'معلق (سقف اعتبار)',
|
SUSPENDED: 'معلق (سقف اعتبار)',
|
||||||
@@ -130,12 +138,14 @@ export class AdminDashboardService {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
const channelLabels: Record<string, string> = { RETAIL: 'تک فروشی', WHOLESALE: 'عمده فروشی' };
|
const channelLabels: Record<string, string> = { RETAIL: 'تک فروشی', WHOLESALE: 'عمده فروشی' };
|
||||||
const channelBreakdown = channelGroups.map((g) => ({
|
const channelBreakdown = viewerIsSuperAdmin
|
||||||
channel: g.channel,
|
? channelGroups.map((g) => ({
|
||||||
label: channelLabels[g.channel] ?? g.channel,
|
channel: g.channel,
|
||||||
count: g._count,
|
label: channelLabels[g.channel] ?? g.channel,
|
||||||
grams: g._sum.totalGoldWeightGrams ?? new Prisma.Decimal(0),
|
count: g._count,
|
||||||
}));
|
grams: g._sum.totalGoldWeightGrams ?? new Prisma.Decimal(0),
|
||||||
|
}))
|
||||||
|
: [];
|
||||||
|
|
||||||
const productTotals = new Map<string, { title: string; weight: Prisma.Decimal; qty: number }>();
|
const productTotals = new Map<string, { title: string; weight: Prisma.Decimal; qty: number }>();
|
||||||
for (const it of topProductRows) {
|
for (const it of topProductRows) {
|
||||||
|
|||||||
Reference in New Issue
Block a user