fix : same values for report and chart

This commit is contained in:
morteza-mortezai
2025-10-29 13:25:58 +03:30
parent 36f7b3c85f
commit a6deda1c0c
2 changed files with 179 additions and 147 deletions
+178 -146
View File
@@ -512,143 +512,6 @@ 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"];
const excludedOrderItemStatuses = [
OrderItemsStatus.cancelled_shop,
OrderItemsStatus.cancelled_system,
OrderItemsStatus.cancelled_user,
OrderItemsStatus.Returned,
];
const pipeline: any[] = [
{
$match: {
createdAt: { $gte: startDate, $lte: endDate },
orderStatus: {
$nin: [OrdersStatus.wait_payment, OrdersStatus.cancelled_system, OrdersStatus.Cancelled],
},
},
},
{
$lookup: {
from: "orderitems",
localField: "_id",
foreignField: "order",
as: "orderItems",
},
},
{
$addFields: {
orderItems: {
$filter: {
input: "$orderItems",
as: "item",
cond: {
$not: [{ $in: ["$$item.status", excludedOrderItemStatuses] }],
},
},
},
},
},
{ $unwind: "$orderItems" },
{
$addFields: {
lineTotal: { $ifNull: ["$orderItems.totalPaymentPrice", 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();
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> {
@@ -2405,13 +2268,32 @@ class OrderItemRepo extends BaseRepository<IOrderItem> {
return salesData;
}
// Shared constants (place near top of file)
async getMonthlySalesReport() {
const EXCLUDED_ORDER_ITEM_STATUSES = [
OrderItemsStatus.cancelled_shop,
OrderItemsStatus.cancelled_system,
OrderItemsStatus.cancelled_user,
OrderItemsStatus.Returned,
];
const EXCLUDED_ORDER_STATUSES = [OrdersStatus.wait_payment, OrdersStatus.cancelled_system, OrdersStatus.Cancelled];
const today = new Date();
const startOfMonth = new Date(today.getFullYear(), today.getMonth(), 1);
const endOfMonth = new Date(today.getFullYear(), today.getMonth() + 1, 1);
const salesData = await this.model.aggregate([
// filter items by month & item status first
{
$match: {
createdAt: { $gte: startOfMonth, $lt: endOfMonth },
status: { $nin: EXCLUDED_ORDER_ITEM_STATUSES },
},
},
// join the parent order
{
$lookup: {
from: "orders",
@@ -2420,31 +2302,181 @@ class OrderItemRepo extends BaseRepository<IOrderItem> {
as: "order",
},
},
{ $unwind: "$order" },
// filter by parent order status
{
$match: {
createdAt: {
$gte: startOfMonth,
$lt: endOfMonth,
},
status: {
$nin: ["cancelled_shop", "cancelled_system", "Returned", OrdersStatus.wait_payment],
},
"order.orderStatus": { $nin: EXCLUDED_ORDER_STATUSES },
},
},
// aggregate totals and collect distinct order ids
{
$group: {
_id: null, // No need for grouping by date since it's filtered for the entire month
_id: null,
totalSales: { $sum: "$totalSellingPrice" },
totalNetSales: { $sum: "$totalRetailPrice" },
itemsSold: { $sum: "$itemsCount" },
totalOrders: { $sum: 1 },
orderIds: { $addToSet: "$order._id" },
},
},
// convert orderIds into a number
{
$addFields: {
totalOrders: { $size: "$orderIds" },
},
},
// optionally remove the array from results
{
$project: { orderIds: 0 },
},
]);
return salesData.length > 0 ? salesData[0] : null;
}
async getOrderChart(granularity: Gran = "daily", from?: string, to?: string) {
const EXCLUDED_ORDER_ITEM_STATUSES = [
OrderItemsStatus.cancelled_shop,
OrderItemsStatus.cancelled_system,
OrderItemsStatus.cancelled_user,
OrderItemsStatus.Returned,
];
const EXCLUDED_ORDER_STATUSES = [OrdersStatus.wait_payment, OrdersStatus.cancelled_system, OrdersStatus.Cancelled];
const DATE_TRUNC_TIMEZONE = "Asia/Tehran";
const MS_PER_DAY = 24 * 60 * 60 * 1000;
const today = new Date();
// if user asked for monthly and did not give from/to, use calendar month
let startDate: Date;
let endDate: Date;
if (granularity === "monthly" && !from && !to) {
startDate = new Date(today.getFullYear(), today.getMonth(), 1);
endDate = new Date(today.getFullYear(), today.getMonth() + 1, 1);
} else {
endDate = to ? new Date(to) : new Date();
endDate.setHours(23, 59, 59, 999);
const defaultStart = new Date(Date.now() - 30 * MS_PER_DAY);
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) {
const tmp = startDate;
startDate = endDate;
endDate = tmp;
}
const allowed: Record<Gran, "day" | "week" | "month"> = {
daily: "day",
weekly: "week",
monthly: "month",
};
const unit = allowed[granularity] ?? "day";
const pipeline: any[] = [
// 1. filter order items by date and item-status
{
$match: {
createdAt: { $gte: startDate, $lte: endDate },
status: { $nin: EXCLUDED_ORDER_ITEM_STATUSES },
},
},
// 2. join parent order and filter by order status
{
$lookup: {
from: "orders",
localField: "order",
foreignField: "_id",
as: "order",
},
},
{ $unwind: "$order" },
{
$match: {
"order.orderStatus": { $nin: EXCLUDED_ORDER_STATUSES },
},
},
// 3. group by truncated period (include timezone)
{
$group: {
_id: {
period: {
$dateTrunc: {
date: "$createdAt",
unit: unit,
binSize: 1,
timezone: DATE_TRUNC_TIMEZONE,
},
},
},
periodTotal: { $sum: "$totalSellingPrice" },
},
},
{ $sort: { "_id.period": 1 } },
];
const aggResults: Array<{ _id: { period: Date }; periodTotal: number }> = await this.model.aggregate(pipeline).exec();
// build map and x/y arrays (same logic as you had)
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);
}
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 },
};
}
async getAnnualSalesReport() {
const today = new Date();
const startOfYear = new Date(today.getFullYear(), 0, 1);
+1 -1
View File
@@ -433,7 +433,7 @@ class OrderService {
//#######################################
//#######################################
async getSaleOrderChart(granularity?: Gran, from?: string, to?: string) {
return this.orderRepo.getOrderChart(granularity, from, to);
return this.orderItemRepo.getOrderChart(granularity, from, to);
}
//#######################################