import { Injectable } from '@nestjs/common'; import { OrderStatus, Prisma } from '@prisma/client'; import { PrismaService } from '../../prisma/prisma.service'; /** * Dashboards — legacy views/DashboardECommerce.vue (`GET get-analytics`) and * views/DashboardAnalytics.vue (`GET get-analytics-tak`). * * Legacy's ECommerce page is a Vuexy template with most of its English demo * cards COMMENTED OUT (Congratulations, Support Tracker, Product Orders, Sales * Stats, Activity Timeline — verified line by line). What is actually live: * - three 30-day stat cards: new customers, gold sold, order count * - "عملکرد کارشناسان فروش": agents ranked by grams sold * - "میزان فروش": sales volume * - "سفارشات هفته جاری": this week's orders * * So this returns exactly those figures rather than the template's demo series. */ @Injectable() export class AdminDashboardService { constructor(private readonly prisma: PrismaService) {} private daysAgo(n: number) { const d = new Date(); d.setDate(d.getDate() - n); return d; } /** Orders that have reached the debt-recognition point count as "sold". */ private static readonly SOLD: Prisma.OrderWhereInput = { status: { in: [OrderStatus.DELIVERED_TO_AGENT, OrderStatus.DELIVERED_TO_CUSTOMER] }, }; /** Legacy `GET get-analytics` — the wholesale dashboard. */ async omdeAnalytics() { const since30 = this.daysAgo(30); const since7 = this.daysAgo(7); const [customerCount, gramsAgg, orderCount, lastOrderRows, agents, monthAgg] = await Promise.all([ // تعداد مشتریان ثبت نامی در 30 روز گذشته this.prisma.customer.count({ where: { createdAt: { gte: since30 } } }), // طلای فروش رفته در 30 روز گذشته this.prisma.order.aggregate({ where: { AND: [AdminDashboardService.SOLD, { createdAt: { gte: since30 } }] }, _sum: { totalGoldWeightGrams: true }, }), // تعداد سفارشات 30 روز گذشته this.prisma.order.count({ where: { createdAt: { gte: since30 } } }), // سفارشات هفته جاری this.prisma.order.findMany({ where: { createdAt: { gte: since7 } }, orderBy: { createdAt: 'desc' }, take: 50, include: { customer: true, branch: true }, }), // عملکرد کارشناسان فروش this.prisma.adminUser.findMany({ where: { role: 'SALES_EXPERT', isActive: true }, select: { id: true, fullName: true }, }), // میزان فروش (same 30-day window, kept separate for clarity) this.prisma.order.aggregate({ where: { AND: [AdminDashboardService.SOLD, { createdAt: { gte: since30 } }] }, _sum: { totalGoldWeightGrams: true }, _count: true, }), ]); // Grams sold per agent, through the customers they referred. The view sorts // this itself by grams_sell, so the shape matters more than the order. const reagent = await Promise.all( agents.map(async (a) => { const agg = await this.prisma.order.aggregate({ where: { AND: [ AdminDashboardService.SOLD, { customer: { referredByAgentId: a.id } }, { createdAt: { gte: since30 } }, ], }, _sum: { totalGoldWeightGrams: true }, _count: true, }); return { id: a.id, name: a.fullName, grams_sell: agg._sum.totalGoldWeightGrams ?? new Prisma.Decimal(0), order_count: agg._count, }; }), ); return { customerCount, allgram: gramsAgg._sum.totalGoldWeightGrams ?? 0, ordercount: orderCount, reagent, lastOrder: lastOrderRows.map((o) => this.orderRow(o)), sumLastOrder: monthAgg._count, sumLastOrderMonth: monthAgg._sum.totalGoldWeightGrams ?? 0, }; } /** Legacy `GET get-analytics-tak` — the retail dashboard. */ async takAnalytics() { const since7 = this.daysAgo(7); const rows = await this.prisma.order.findMany({ where: { createdAt: { gte: since7 } }, orderBy: { createdAt: 'desc' }, take: 50, include: { customer: true, branch: true }, }); // The retail view reads only `model.lastOrder`; the rest of its cards are // the Vuexy demo series and are not rendered from server data. return { lastOrder: rows.map((o) => this.orderRow(o)) }; } /** * Legacy `report/order_item` — "گزارش تجمیعی سفارشات و اسناد مالی": order * lines aggregated over a date range. */ async orderItemReport(from?: string, to?: string) { const where = this.dateWhere(from, to); const orders = await this.prisma.order.findMany({ where, include: { customer: true, items: { include: { productVariant: { include: { product: true } } } }, }, orderBy: { createdAt: 'desc' }, }); const rows = orders.flatMap((o) => o.items.map((it) => ({ order_id: o.id, order_number: o.orderNumber, customer: o.customer.fullName, product: it.productVariant.product.title, variant: it.productVariant.title, qty: it.quantity, w: it.computedWeightGrams, total_w: it.computedWeightGrams.mul(it.quantity), caj: o.createdAt, })), ); const totalWeight = rows.reduce( (s, r) => s.add(r.total_w), new Prisma.Decimal(0), ); return { rows, summary: { order_count: orders.length, item_count: rows.length, total_weight: totalWeight, }, }; } /** Legacy `report/product` — per-product sales over a date range. */ async productReport(from?: string, to?: string) { const where = this.dateWhere(from, to); const orders = await this.prisma.order.findMany({ where, include: { items: { include: { productVariant: { include: { product: true } } } } }, }); const byProduct = new Map< string, { id: string; title: string; qty: number; weight: Prisma.Decimal; orders: Set } >(); for (const o of orders) { for (const it of o.items) { const p = it.productVariant.product; const row = byProduct.get(p.id) ?? { id: p.id, title: p.title, qty: 0, weight: new Prisma.Decimal(0), orders: new Set(), }; row.qty += it.quantity; row.weight = row.weight.add(it.computedWeightGrams.mul(it.quantity)); row.orders.add(o.id); byProduct.set(p.id, row); } } const rows = [...byProduct.values()] .map((r) => ({ id: r.id, title: r.title, total: r.qty, w: r.weight, order_count: r.orders.size, })) .sort((a, b) => Number(b.w) - Number(a.w)); return { rows, summary: { product_count: rows.length } }; } /** Shared date filter for both reports; an open end stays open. */ private dateWhere(from?: string, to?: string): Prisma.OrderWhereInput { if (!from && !to) return {}; const createdAt: Prisma.DateTimeFilter = {}; if (from) createdAt.gte = new Date(from); if (to) { // Include the whole end day, not just its midnight. const end = new Date(to); end.setHours(23, 59, 59, 999); createdAt.lte = end; } return { createdAt }; } /** The row shape both dashboards' tables read. */ private orderRow( o: Prisma.OrderGetPayload<{ include: { customer: true; branch: true } }>, ) { const labels: Record = { SUSPENDED: 'معلق (سقف اعتبار)', PLACED: 'ثبت شده', ADMIN_APPROVED: 'تایید مدیر', SOURCING: 'در حال تامین', PREPARING: 'در حال آماده‌سازی', DELIVERED_TO_AGENT: 'تحویل به نماینده', DELIVERED_TO_CUSTOMER: 'تحویل به مشتری', CANCELLED: 'لغو شده', }; // legacy reads `statusColor` straight off the row const colors: Record = { SUSPENDED: 'warning', PLACED: 'primary', ADMIN_APPROVED: 'primary', SOURCING: 'warning', PREPARING: 'warning', DELIVERED_TO_AGENT: 'success', DELIVERED_TO_CUSTOMER: 'success', CANCELLED: 'danger', }; return { id: o.id, name: o.orderNumber, customer: o.customer.fullName, w: o.totalGoldWeightGrams, sts_str: labels[o.status], statusColor: colors[o.status], caj: o.createdAt, // legacy rendered a parent/children tree for split orders; the redesigned // schema has no such relation, so both are empty rather than faked. parent: null, children: [], }; } }