From f62f7cd2a77b0ee766f8be675c353ec5766c8e4c Mon Sep 17 00:00:00 2001 From: morteza-mortezai Date: Thu, 18 Jun 2026 21:52:16 +0330 Subject: [PATCH] order report by food --- .../orders/controllers/orders.controller.ts | 9 ++++ .../orders/dto/food-order-report.dto.ts | 36 +++++++++++++ .../orders/providers/orders.service.ts | 51 +++++++++++++++++++ 3 files changed, 96 insertions(+) create mode 100644 src/modules/orders/dto/food-order-report.dto.ts diff --git a/src/modules/orders/controllers/orders.controller.ts b/src/modules/orders/controllers/orders.controller.ts index 858cf8c..17b7ac4 100644 --- a/src/modules/orders/controllers/orders.controller.ts +++ b/src/modules/orders/controllers/orders.controller.ts @@ -12,6 +12,7 @@ import { UpdateOrderStatusDto } from '../dto/update-order-status.dto'; import { Permissions } from 'src/common/decorators/permissions.decorator'; import { Permission } from 'src/common/enums/permission.enum'; import { AdminCreateOrderDto } from '../dto/admin-create-order.dto'; +import { FoodOrderReportDto } from '../dto/food-order-report.dto'; @ApiTags('orders') @ApiBearerAuth() @@ -127,4 +128,12 @@ export class OrdersController { getFoodSalesPieChart(@RestId() restId: string) { return this.ordersService.getFoodSalesPieChart(restId); } + + @UseGuards(AdminAuthGuard) + @Permissions(Permission.VIEW_REPORTS) + @ApiOperation({ summary: 'Get order report grouped by food' }) + @Get('admin/orders/report/by-food') + getFoodOrderReport(@RestId() restId: string, @Query() dto: FoodOrderReportDto) { + return this.ordersService.getFoodOrderReport(restId, dto); + } } diff --git a/src/modules/orders/dto/food-order-report.dto.ts b/src/modules/orders/dto/food-order-report.dto.ts new file mode 100644 index 0000000..aba9815 --- /dev/null +++ b/src/modules/orders/dto/food-order-report.dto.ts @@ -0,0 +1,36 @@ +import { IsOptional, IsDateString, IsEnum } from 'class-validator'; +import { ApiPropertyOptional } from '@nestjs/swagger'; + +export enum FoodOrderReportSortBy { + Count = 'count', + Price = 'price', +} + +export class FoodOrderReportDto { + @ApiPropertyOptional({ + format: 'date-time', + description: 'Filter orders from this date and time', + example: '2024-01-01T08:00:00.000Z', + }) + @IsOptional() + @IsDateString() + from?: string; + + @ApiPropertyOptional({ + format: 'date-time', + description: 'Filter orders until this date and time', + example: '2024-12-31T23:59:59.000Z', + }) + @IsOptional() + @IsDateString() + to?: string; + + @ApiPropertyOptional({ + enum: FoodOrderReportSortBy, + default: FoodOrderReportSortBy.Count, + description: 'Sort results by order count or food price', + }) + @IsOptional() + @IsEnum(FoodOrderReportSortBy) + sortBy: FoodOrderReportSortBy = FoodOrderReportSortBy.Count; +} diff --git a/src/modules/orders/providers/orders.service.ts b/src/modules/orders/providers/orders.service.ts index c400cbf..90b471e 100644 --- a/src/modules/orders/providers/orders.service.ts +++ b/src/modules/orders/providers/orders.service.ts @@ -25,6 +25,7 @@ import { EventEmitter2 } from '@nestjs/event-emitter'; import { OrderCreatedEvent, OrderStatusChangedEvent } from '../events/order.events'; import { OrderMessage } from 'src/common/enums/message.enum'; import { AdminCreateOrderDto } from '../dto/admin-create-order.dto'; +import { FoodOrderReportDto, FoodOrderReportSortBy } from '../dto/food-order-report.dto'; import { normalizePhone } from 'src/modules/utils/phone.util'; type OrderItemData = { food: Food; quantity: number; unitPrice: number; discount: number }; @@ -766,4 +767,54 @@ console.log(cart); }; }); } + + async getFoodOrderReport(restId: string, dto: FoodOrderReportDto) { + const { from, to, sortBy = FoodOrderReportSortBy.Count } = dto; + + const params: unknown[] = [restId]; + const dateFilters: string[] = []; + + if (from) { + params.push(new Date(from)); + dateFilters.push(`AND o.created_at >= ?`); + } + + if (to) { + params.push(new Date(to)); + dateFilters.push(`AND o.created_at <= ?`); + } + + const orderColumn = sortBy === FoodOrderReportSortBy.Price ? 'f.price' : 'count'; + + const result = await this.em.execute( + ` + SELECT + f.id as food_id, + f.title as food_title, + f.price as food_price, + COALESCE(SUM(oi.quantity), 0)::int as count, + COALESCE(SUM(oi.total_price), 0)::numeric as total_price + FROM order_items oi + INNER JOIN orders o ON oi.order_id = o.id + INNER JOIN foods f ON oi.food_id = f.id + WHERE o.restaurant_id = ? + ${dateFilters.join('\n ')} + AND o.status NOT IN ('pendingPayment', 'canceled') + GROUP BY f.id, f.title, f.price + HAVING SUM(oi.quantity) > 0 + ORDER BY ${orderColumn} DESC + `, + params, + ); + + return result.map((row: Record) => ({ + food: { + id: row.food_id, + title: row.food_title, + }, + count: Number(row.count || 0), + price: Number(row.food_price || 0), + totalPrice: Number(row.total_price || 0), + })); + } }