add: order chart
This commit is contained in:
@@ -12,6 +12,7 @@ import { IOCTYPES } from "../../../IOC/ioc.types";
|
|||||||
import { CancelService } from "../../cancel/cancel.service";
|
import { CancelService } from "../../cancel/cancel.service";
|
||||||
import { CreateCancelDTO } from "../../cancel/DTO/cancel.dto";
|
import { CreateCancelDTO } from "../../cancel/DTO/cancel.dto";
|
||||||
import { CancelReasonDTO } from "../../cancel/DTO/cancelReason.dto";
|
import { CancelReasonDTO } from "../../cancel/DTO/cancelReason.dto";
|
||||||
|
import { Gran } from "../../order/order.repository";
|
||||||
import { OrderService } from "../../order/order.service";
|
import { OrderService } from "../../order/order.service";
|
||||||
import { ReasonDTO, UpdateReasonDTO } from "../../return/DTO/returnReason.dto";
|
import { ReasonDTO, UpdateReasonDTO } from "../../return/DTO/returnReason.dto";
|
||||||
import { ReturnService } from "../../return/return.service";
|
import { ReturnService } from "../../return/return.service";
|
||||||
@@ -248,4 +249,16 @@ export class AdminOrderController extends BaseController {
|
|||||||
const data = await this.orderService.getSellerOrderDetailAsAdmin(+orderId);
|
const data = await this.orderService.getSellerOrderDetailAsAdmin(+orderId);
|
||||||
return this.response(data);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { BaseRepository } from "../../common/base/repository";
|
|||||||
import { OrderItemsStatus, OrdersStatus } from "../../common/enums/order.enum";
|
import { OrderItemsStatus, OrdersStatus } from "../../common/enums/order.enum";
|
||||||
import { OrderStatusQuery, SellerOrdersQueries } from "../../common/types/query.type";
|
import { OrderStatusQuery, SellerOrdersQueries } from "../../common/types/query.type";
|
||||||
import { TimeService } from "../../utils/time.service";
|
import { TimeService } from "../../utils/time.service";
|
||||||
|
export type Gran = "daily" | "weekly" | "monthly";
|
||||||
|
|
||||||
class OrderRepository extends BaseRepository<IOrder> {
|
class OrderRepository extends BaseRepository<IOrder> {
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -502,6 +503,126 @@ class OrderRepository extends BaseRepository<IOrder> {
|
|||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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<Gran, "day" | "week" | "month"> = {
|
||||||
|
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<string, number>();
|
||||||
|
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<IOrderItem> {
|
class OrderItemRepo extends BaseRepository<IOrderItem> {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { SellerOrdersDTO } from "./DTO/sellerOrder.dto";
|
|||||||
import { UserOrderDTO } from "./DTO/userOrder.dto";
|
import { UserOrderDTO } from "./DTO/userOrder.dto";
|
||||||
import { IShipmentAddress } from "./models/Abstraction/IOrder";
|
import { IShipmentAddress } from "./models/Abstraction/IOrder";
|
||||||
import { IOrderItem } from "./models/Abstraction/IOrderItem";
|
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 { AddressMessage, OrderMessage, ShopMessage, UserMessage } from "../../common/enums/message.enum";
|
||||||
import { OrderItemsStatus, OrdersStatus } from "../../common/enums/order.enum";
|
import { OrderItemsStatus, OrdersStatus } from "../../common/enums/order.enum";
|
||||||
import { OrderStatusQuery, SellerOrdersQueries } from "../../common/types/query.type";
|
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() {
|
async getDailySalesReport() {
|
||||||
|
|||||||
Reference in New Issue
Block a user