diff --git a/src/modules/admin/controllers/order.controller.ts b/src/modules/admin/controllers/order.controller.ts index f928199..fbf0787 100644 --- a/src/modules/admin/controllers/order.controller.ts +++ b/src/modules/admin/controllers/order.controller.ts @@ -12,6 +12,7 @@ import { IOCTYPES } from "../../../IOC/ioc.types"; import { CancelService } from "../../cancel/cancel.service"; import { CreateCancelDTO } from "../../cancel/DTO/cancel.dto"; import { CancelReasonDTO } from "../../cancel/DTO/cancelReason.dto"; +import { Gran } from "../../order/order.repository"; import { OrderService } from "../../order/order.service"; import { ReasonDTO, UpdateReasonDTO } from "../../return/DTO/returnReason.dto"; import { ReturnService } from "../../return/return.service"; @@ -248,4 +249,16 @@ export class AdminOrderController extends BaseController { const data = await this.orderService.getSellerOrderDetailAsAdmin(+orderId); return this.response(data); } + + @ApiOperation("get an sale order chart") + @ApiResponse("successful", HttpStatus.Ok) + @ApiQuery("from", "starting date") + @ApiQuery("to", "ending date") + @ApiQuery("granularity", "daily weekly monthly") + @ApiAuth() + @httpGet("/chart", Guard.authAdmin(), Guard.checkAdminPermissions([PermissionEnum.ORDER])) + public async getchart(@queryParam("granularity") granularity: Gran, @queryParam("from") from: string, @queryParam("to") to: string) { + const data = await this.orderService.getSaleOrderChart(granularity, from, to); + return this.response(data); + } } diff --git a/src/modules/order/order.repository.ts b/src/modules/order/order.repository.ts index 2b5c06c..b67a2c5 100644 --- a/src/modules/order/order.repository.ts +++ b/src/modules/order/order.repository.ts @@ -8,6 +8,7 @@ import { BaseRepository } from "../../common/base/repository"; import { OrderItemsStatus, OrdersStatus } from "../../common/enums/order.enum"; import { OrderStatusQuery, SellerOrdersQueries } from "../../common/types/query.type"; import { TimeService } from "../../utils/time.service"; +export type Gran = "daily" | "weekly" | "monthly"; class OrderRepository extends BaseRepository { constructor() { @@ -502,6 +503,126 @@ class OrderRepository extends BaseRepository { }, ]); } + + async getOrderChart(granularity: Gran = "daily", from?: string, to?: string) { + // تبدیل/نرمالایز تاریخ‌ها + const MS_PER_DAY = 24 * 60 * 60 * 1000; + const endDate = to ? new Date(to) : new Date(); + endDate.setHours(23, 59, 59, 999); + + const defaultStart = new Date(Date.now() - 30 * MS_PER_DAY); + const startDate = from ? new Date(from) : defaultStart; + startDate.setHours(0, 0, 0, 0); + + if (isNaN(startDate.getTime()) || isNaN(endDate.getTime())) { + throw new Error("Invalid date format. Expected YYYY-MM-DD or ISO date."); + } + + if (startDate > endDate) { + // swap + const tmp = new Date(startDate); + (startDate as any) = new Date(endDate); + (endDate as any) = tmp; + } + + // validate granularity + const allowed: Record = { + daily: "day", + weekly: "week", + monthly: "month", + }; + const unit = allowed[granularity] ?? allowed["daily"]; + + // Aggregation pipeline + const pipeline: any[] = [ + { + $match: { + createdAt: { $gte: startDate, $lte: endDate }, + // فقط سفارش‌های ثبت شده/پرداخت شده — تنظیم کن اگر می‌خواهی فیلتر فرق کند + orderStatus: { $ne: OrdersStatus.wait_payment }, + }, + }, + { + $lookup: { + from: "orderitems", // اگر اسم کالکشنت فرق دارد اینجا اصلاح کن + localField: "_id", + foreignField: "order", + as: "items", + }, + }, + { $unwind: { path: "$items", preserveNullAndEmptyArrays: false } }, + { + $addFields: { + lineTotal: { + $multiply: [{ $ifNull: ["$items.quantity", 0] }, { $ifNull: ["$items.price", 0] }], + }, + }, + }, + { + $group: { + _id: { + period: { + $dateTrunc: { + date: "$createdAt", + unit: unit, + binSize: 1, + }, + }, + }, + periodTotal: { $sum: "$lineTotal" }, + }, + }, + { $sort: { "_id.period": 1 } }, + ]; + + const aggResults: Array<{ _id: { period: Date }; periodTotal: number }> = await this.model.aggregate(pipeline).exec(); + + // map نتایج + const map = new Map(); + for (const r of aggResults) { + const d: Date = r._id.period; + const key = unit === "month" ? d.toISOString().slice(0, 7) : d.toISOString().slice(0, 10); + map.set(key, r.periodTotal ?? 0); + } + + // helper برای قدم زدن در دوره‌ها + function formatKey(d: Date) { + return unit === "month" ? d.toISOString().slice(0, 7) : d.toISOString().slice(0, 10); + } + function addPeriod(d: Date) { + const nd = new Date(d); + if (unit === "day") nd.setDate(nd.getDate() + 1); + else if (unit === "week") nd.setDate(nd.getDate() + 7); + else nd.setMonth(nd.getMonth() + 1); + return nd; + } + + const xAxis: string[] = []; + const yAxis: number[] = []; + + let cursor = new Date(startDate); + if (unit === "month") cursor.setDate(1); + + while (cursor <= endDate) { + const key = formatKey(cursor); + xAxis.push(key); + yAxis.push(map.get(key) ?? 0); + cursor = addPeriod(cursor); + } + + const totalSales = yAxis.reduce((s, v) => s + v, 0); + + return { + chartType: "line", + title: "نمودار کل فروش", + unit: "تومان", + granularity, + xAxis, + yAxis, + totalSales, + _debug: { aggregatedPeriods: aggResults.length }, + }; + } } class OrderItemRepo extends BaseRepository { diff --git a/src/modules/order/order.service.ts b/src/modules/order/order.service.ts index 84d05a4..3f8c40f 100644 --- a/src/modules/order/order.service.ts +++ b/src/modules/order/order.service.ts @@ -7,7 +7,7 @@ import { SellerOrdersDTO } from "./DTO/sellerOrder.dto"; import { UserOrderDTO } from "./DTO/userOrder.dto"; import { IShipmentAddress } from "./models/Abstraction/IOrder"; import { IOrderItem } from "./models/Abstraction/IOrderItem"; -import { OrderItemRepo, OrderRepository } from "./order.repository"; +import { Gran, OrderItemRepo, OrderRepository } from "./order.repository"; import { AddressMessage, OrderMessage, ShopMessage, UserMessage } from "../../common/enums/message.enum"; import { OrderItemsStatus, OrdersStatus } from "../../common/enums/order.enum"; import { OrderStatusQuery, SellerOrdersQueries } from "../../common/types/query.type"; @@ -430,6 +430,12 @@ class OrderService { }; } + //####################################### + //####################################### + async getSaleOrderChart(granularity?: Gran, from?: string, to?: string) { + return this.orderRepo.getOrderChart(granularity, from, to); + } + //####################################### //####################################### async getDailySalesReport() {