From 13f21d4d3f25665d98dc4f4fec2df6b3698d6ef0 Mon Sep 17 00:00:00 2001 From: morteza-mortezai Date: Tue, 14 Jul 2026 19:59:20 +0330 Subject: [PATCH] shift --- src/common/enums/message.enum.ts | 1 + src/common/enums/permission.enum.ts | 6 + .../controllers/cash-shifts.controller.ts | 60 +++++----- .../orders/controllers/orders.controller.ts | 4 +- .../orders/dto/update-cash-shift.dto.ts | 17 --- .../orders/providers/cash-shifts.service.ts | 107 ++++++++++-------- .../repositories/cash-shift.repository.ts | 16 ++- .../orders/repositories/order.repository.ts | 52 +-------- src/seeders/data/roles.data.ts | 5 +- 9 files changed, 115 insertions(+), 153 deletions(-) delete mode 100644 src/modules/orders/dto/update-cash-shift.dto.ts diff --git a/src/common/enums/message.enum.ts b/src/common/enums/message.enum.ts index 2ff5011..5bd9394 100755 --- a/src/common/enums/message.enum.ts +++ b/src/common/enums/message.enum.ts @@ -792,6 +792,7 @@ export const enum CashShiftMessage { NOT_FOUND = 'شیفت صندوق یافت نشد', ALREADY_OPEN = 'یک شیفت صندوق باز از قبل وجود دارد', ALREADY_CLOSED = 'این شیفت صندوق قبلا بسته شده است', + ACCESS_DENIED = 'شما به این شیفت صندوق دسترسی ندارید', ADMIN_NOT_FOUND = 'مدیر یافت نشد', RESTAURANT_NOT_FOUND = 'رستوران یافت نشد', } diff --git a/src/common/enums/permission.enum.ts b/src/common/enums/permission.enum.ts index 63060da..4e9f284 100644 --- a/src/common/enums/permission.enum.ts +++ b/src/common/enums/permission.enum.ts @@ -10,6 +10,9 @@ export enum Permission { // Order Management MANAGE_ORDERS = 'manage_orders', + VIEW_ORDER_REPORTS = 'view_order_reports', + MANAGE_SHIFTS = 'manage_shifts', + VIEW_ALL_SHIFTS = 'view_all_shifts', // User & Admin Management MANAGE_ADMINS = 'manage_admins', @@ -46,6 +49,9 @@ export const PermissionTitles: Record = { [Permission.MANAGE_FOODS]: 'مدیریت غذاها', [Permission.MANAGE_CATEGORIES]: 'مدیریت دسته‌بندی‌ها', [Permission.MANAGE_ORDERS]: 'مدیریت سفارشات', + [Permission.VIEW_ORDER_REPORTS]: 'مشاهده گزارشات سفارش', + [Permission.MANAGE_SHIFTS]: 'مدیریت شیفت‌ها', + [Permission.VIEW_ALL_SHIFTS]: 'مشاهده همه شیفت‌ها', [Permission.MANAGE_ADMINS]: 'مدیریت مدیران', [Permission.MANAGE_USERS]: 'مدیریت کاربران', [Permission.VIEW_REPORTS]: 'مشاهده گزارشات', diff --git a/src/modules/orders/controllers/cash-shifts.controller.ts b/src/modules/orders/controllers/cash-shifts.controller.ts index 80046e7..2c3e7ac 100644 --- a/src/modules/orders/controllers/cash-shifts.controller.ts +++ b/src/modules/orders/controllers/cash-shifts.controller.ts @@ -9,7 +9,6 @@ import { Permission } from 'src/common/enums/permission.enum'; import { OpenCashShiftDto } from '../dto/open-cash-shift.dto'; import { CloseCashShiftDto } from '../dto/close-cash-shift.dto'; import { FindCashShiftsDto } from '../dto/find-cash-shifts.dto'; -import { UpdateCashShiftDto } from '../dto/update-cash-shift.dto'; @ApiTags('cash-shifts') @ApiBearerAuth() @@ -18,7 +17,7 @@ export class CashShiftsController { constructor(private readonly cashShiftsService: CashShiftsService) {} @UseGuards(AdminAuthGuard) - @Permissions(Permission.MANAGE_ORDERS) + @Permissions(Permission.MANAGE_SHIFTS) @Post('admin/cash-shifts/open') @ApiOperation({ summary: 'Open a new cash shift' }) @ApiBody({ type: OpenCashShiftDto }) @@ -27,21 +26,7 @@ export class CashShiftsController { } @UseGuards(AdminAuthGuard) - @Permissions(Permission.MANAGE_ORDERS) - @Patch('admin/cash-shifts/:shiftId') - @ApiOperation({ summary: 'Update a cash shift' }) - @ApiParam({ name: 'shiftId', description: 'Cash shift ID' }) - @ApiBody({ type: UpdateCashShiftDto }) - updateShift( - @Param('shiftId') shiftId: string, - @RestId() restId: string, - @Body() dto: UpdateCashShiftDto, - ) { - return this.cashShiftsService.updateShift(shiftId, restId, dto); - } - - @UseGuards(AdminAuthGuard) - @Permissions(Permission.MANAGE_ORDERS) + @Permissions(Permission.MANAGE_SHIFTS) @Patch('admin/cash-shifts/:shiftId/close') @ApiOperation({ summary: 'Close an open cash shift' }) @ApiParam({ name: 'shiftId', description: 'Cash shift ID' }) @@ -49,50 +34,59 @@ export class CashShiftsController { closeShift( @Param('shiftId') shiftId: string, @RestId() restId: string, + @AdminId() adminId: string, @Body() dto: CloseCashShiftDto, ) { - return this.cashShiftsService.closeShift(shiftId, restId, dto); + return this.cashShiftsService.closeShift(shiftId, restId, adminId, dto); } @UseGuards(AdminAuthGuard) - @Permissions(Permission.MANAGE_ORDERS) + @Permissions(Permission.MANAGE_SHIFTS) @Get('admin/cash-shifts/current/summary') @ApiOperation({ summary: 'Calculate summary for the current open cash shift' }) - getCurrentShiftSummary(@RestId() restId: string) { - return this.cashShiftsService.calculateCurrentShiftSummary(restId); + getCurrentShiftSummary(@RestId() restId: string, @AdminId() adminId: string) { + return this.cashShiftsService.calculateCurrentShiftSummary(restId, adminId); } @UseGuards(AdminAuthGuard) - @Permissions(Permission.MANAGE_ORDERS) + @Permissions(Permission.MANAGE_SHIFTS) @Get('admin/cash-shifts/current') @ApiOperation({ summary: 'Get the current open cash shift' }) - getCurrentOpenShift(@RestId() restId: string) { - return this.cashShiftsService.getCurrentOpenShift(restId); + getCurrentOpenShift(@RestId() restId: string, @AdminId() adminId: string) { + return this.cashShiftsService.getCurrentOpenShift(restId, adminId); } @UseGuards(AdminAuthGuard) - @Permissions(Permission.MANAGE_ORDERS) + @Permissions(Permission.MANAGE_SHIFTS) @Get('admin/cash-shifts') @ApiOperation({ summary: 'List cash shifts with pagination and filters' }) - findAll(@RestId() restId: string, @Query() dto: FindCashShiftsDto) { - return this.cashShiftsService.findAll(restId, dto); + findAll(@RestId() restId: string, @AdminId() adminId: string, @Query() dto: FindCashShiftsDto) { + return this.cashShiftsService.findAll(restId, adminId, dto); } @UseGuards(AdminAuthGuard) - @Permissions(Permission.MANAGE_ORDERS) + @Permissions(Permission.MANAGE_SHIFTS) @Get('admin/cash-shifts/:shiftId/summary') @ApiOperation({ summary: 'Calculate summary for a cash shift' }) @ApiParam({ name: 'shiftId', description: 'Cash shift ID' }) - getShiftSummary(@Param('shiftId') shiftId: string, @RestId() restId: string) { - return this.cashShiftsService.calculateShiftSummary(shiftId, restId); + getShiftSummary( + @Param('shiftId') shiftId: string, + @RestId() restId: string, + @AdminId() adminId: string, + ) { + return this.cashShiftsService.calculateShiftSummary(shiftId, restId, adminId); } @UseGuards(AdminAuthGuard) - @Permissions(Permission.MANAGE_ORDERS) + @Permissions(Permission.MANAGE_SHIFTS) @Get('admin/cash-shifts/:shiftId') @ApiOperation({ summary: 'Get a cash shift by ID' }) @ApiParam({ name: 'shiftId', description: 'Cash shift ID' }) - findOne(@Param('shiftId') shiftId: string, @RestId() restId: string) { - return this.cashShiftsService.findOne(shiftId, restId); + findOne( + @Param('shiftId') shiftId: string, + @RestId() restId: string, + @AdminId() adminId: string, + ) { + return this.cashShiftsService.findOne(shiftId, restId, adminId); } } diff --git a/src/modules/orders/controllers/orders.controller.ts b/src/modules/orders/controllers/orders.controller.ts index 1fab77f..7003f56 100644 --- a/src/modules/orders/controllers/orders.controller.ts +++ b/src/modules/orders/controllers/orders.controller.ts @@ -146,7 +146,7 @@ export class OrdersController { } @UseGuards(AdminAuthGuard) - @Permissions(Permission.VIEW_REPORTS) + @Permissions(Permission.VIEW_ORDER_REPORTS) @ApiOperation({ summary: 'Get order report grouped by food' }) @Get('admin/orders/report/by-food') getFoodOrderReport(@RestId() restId: string, @Query() dto: FoodOrderReportDto) { @@ -154,7 +154,7 @@ export class OrdersController { } @UseGuards(AdminAuthGuard) - @Permissions(Permission.VIEW_REPORTS) + @Permissions(Permission.VIEW_ORDER_REPORTS) @ApiOperation({ summary: 'Get order report grouped by day' }) @Get('admin/orders/report/by-day') getDailyOrderReport(@RestId() restId: string, @Query() dto: DailyOrderReportDto) { diff --git a/src/modules/orders/dto/update-cash-shift.dto.ts b/src/modules/orders/dto/update-cash-shift.dto.ts deleted file mode 100644 index 3d28a12..0000000 --- a/src/modules/orders/dto/update-cash-shift.dto.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { ApiPropertyOptional } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; -import { IsNumber, IsOptional, IsString, Min } from 'class-validator'; - -export class UpdateCashShiftDto { - @ApiPropertyOptional({ description: 'Opening cash amount in drawer', minimum: 0 }) - @Type(() => Number) - @IsOptional() - @IsNumber() - @Min(0) - openingAmount?: number; - - @ApiPropertyOptional() - @IsOptional() - @IsString() - notes?: string; -} diff --git a/src/modules/orders/providers/cash-shifts.service.ts b/src/modules/orders/providers/cash-shifts.service.ts index 07dfc50..215a611 100644 --- a/src/modules/orders/providers/cash-shifts.service.ts +++ b/src/modules/orders/providers/cash-shifts.service.ts @@ -1,4 +1,4 @@ -import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common'; +import { BadRequestException, ForbiddenException, Injectable, NotFoundException } from '@nestjs/common'; import { EntityManager } from '@mikro-orm/postgresql'; import { CashShift } from '../entities/cash-shift.entity'; import { CashShiftStatus, CashShiftSummary } from '../interface/cash-shift.interface'; @@ -6,19 +6,21 @@ import { CashShiftRepository } from '../repositories/cash-shift.repository'; import { OpenCashShiftDto } from '../dto/open-cash-shift.dto'; import { CloseCashShiftDto } from '../dto/close-cash-shift.dto'; import { FindCashShiftsDto } from '../dto/find-cash-shifts.dto'; -import { UpdateCashShiftDto } from '../dto/update-cash-shift.dto'; import { Admin } from '../../admin/entities/admin.entity'; import { Restaurant } from '../../restaurants/entities/restaurant.entity'; import { Order } from '../entities/order.entity'; import { CashShiftMessage } from 'src/common/enums/message.enum'; +import { Permission } from 'src/common/enums/permission.enum'; import { PaginatedResult } from 'src/common/interfaces/pagination.interface'; import { calculateCashShiftSummary } from '../utils/calculate-cash-shift-summary.util'; +import { PermissionsService } from 'src/modules/roles/providers/permissions.service'; @Injectable() export class CashShiftsService { constructor( private readonly em: EntityManager, private readonly cashShiftRepository: CashShiftRepository, + private readonly permissionsService: PermissionsService, ) {} async openShift(restaurantId: string, adminId: string, dto: OpenCashShiftDto): Promise { @@ -54,7 +56,12 @@ export class CashShiftsService { return shift; } - async updateShift(shiftId: string, restaurantId: string, dto: UpdateCashShiftDto): Promise { + async closeShift( + shiftId: string, + restaurantId: string, + adminId: string, + dto: CloseCashShiftDto, + ): Promise { const shift = await this.em.findOne( CashShift, { id: shiftId, restaurant: { id: restaurantId } }, @@ -65,40 +72,13 @@ export class CashShiftsService { throw new NotFoundException(CashShiftMessage.NOT_FOUND); } - if (dto.openingAmount !== undefined) { - shift.openingAmount = dto.openingAmount; + await this.assertShiftAccess(shift, adminId, restaurantId); - if (shift.status === CashShiftStatus.CLOSED && shift.countedAmount !== null) { - const summary = await this.calculateShiftSummary(shiftId, restaurantId); - shift.expectedAmount = summary.expectedAmount; - shift.differenceAmount = Number(shift.countedAmount) - summary.expectedAmount; - shift.ordersCount = summary.ordersCount; - } - } - - if (dto.notes !== undefined) { - shift.notes = dto.notes; - } - - await this.em.persistAndFlush(shift); - return shift; - } - - async closeShift(shiftId: string, restaurantId: string, dto: CloseCashShiftDto): Promise { - const shift = await this.em.findOne( - CashShift, - { id: shiftId, restaurant: { id: restaurantId } }, - { populate: ['admin', 'restaurant'] }, - ); - - if (!shift) { - throw new NotFoundException(CashShiftMessage.NOT_FOUND); - } if (shift.status === CashShiftStatus.CLOSED) { throw new BadRequestException(CashShiftMessage.ALREADY_CLOSED); } - const summary = await this.calculateShiftSummary(shiftId, restaurantId); + const summary = await this.calculateShiftSummary(shiftId, restaurantId, adminId); const countedAmount = dto.countedAmount; shift.expectedAmount = summary.expectedAmount; @@ -115,18 +95,21 @@ export class CashShiftsService { return shift; } - async calculateShiftSummary(shiftId: string, restaurantId: string): Promise { - const shift = await this.em.findOne(CashShift, { id: shiftId, restaurant: { id: restaurantId } }); - if (!shift) { - throw new NotFoundException(CashShiftMessage.NOT_FOUND); - } - + async calculateShiftSummary( + shiftId: string, + restaurantId: string, + adminId: string, + ): Promise { + const shift = await this.findOne(shiftId, restaurantId, adminId); const orders = await this.getShiftOrders(shift, restaurantId); return calculateCashShiftSummary(Number(shift.openingAmount), orders); } - async calculateCurrentShiftSummary(restaurantId: string): Promise { - const shift = await this.cashShiftRepository.findOpenShift(restaurantId); + async calculateCurrentShiftSummary( + restaurantId: string, + adminId: string, + ): Promise { + const shift = await this.getCurrentOpenShift(restaurantId, adminId); if (!shift) { return null; } @@ -135,11 +118,22 @@ export class CashShiftsService { return calculateCashShiftSummary(Number(shift.openingAmount), orders); } - findAll(restaurantId: string, dto: FindCashShiftsDto): Promise> { - return this.cashShiftRepository.findAllPaginated(restaurantId, dto); + async findAll( + restaurantId: string, + adminId: string, + dto: FindCashShiftsDto, + ): Promise> { + const canViewAll = await this.canViewAllShifts(adminId, restaurantId); + const query = { ...dto }; + + if (!canViewAll) { + query.adminId = adminId; + } + + return this.cashShiftRepository.findAllPaginated(restaurantId, query); } - async findOne(shiftId: string, restaurantId: string): Promise { + async findOne(shiftId: string, restaurantId: string, adminId: string): Promise { const shift = await this.em.findOne( CashShift, { id: shiftId, restaurant: { id: restaurantId } }, @@ -150,11 +144,13 @@ export class CashShiftsService { throw new NotFoundException(CashShiftMessage.NOT_FOUND); } + await this.assertShiftAccess(shift, adminId, restaurantId); return shift; } - getCurrentOpenShift(restaurantId: string): Promise { - return this.cashShiftRepository.findOpenShift(restaurantId); + async getCurrentOpenShift(restaurantId: string, adminId: string): Promise { + const canViewAll = await this.canViewAllShifts(adminId, restaurantId); + return this.cashShiftRepository.findOpenShift(restaurantId, canViewAll ? undefined : adminId); } async assignOrderToOpenShift(order: Order, em: EntityManager = this.em): Promise { @@ -185,4 +181,25 @@ export class CashShiftsService { { populate: ['payments'] }, ); } + + private async canViewAllShifts(adminId: string, restaurantId: string): Promise { + const permissions = await this.permissionsService.getAdminPermissions(adminId, restaurantId); + return permissions.includes(Permission.VIEW_ALL_SHIFTS); + } + + private async assertShiftAccess( + shift: CashShift, + adminId: string, + restaurantId: string, + ): Promise { + const canViewAll = await this.canViewAllShifts(adminId, restaurantId); + if (canViewAll) { + return; + } + + const shiftAdminId = typeof shift.admin === 'string' ? shift.admin : shift.admin.id; + if (shiftAdminId !== adminId) { + throw new ForbiddenException(CashShiftMessage.ACCESS_DENIED); + } + } } diff --git a/src/modules/orders/repositories/cash-shift.repository.ts b/src/modules/orders/repositories/cash-shift.repository.ts index f6b35bd..c4ff19b 100644 --- a/src/modules/orders/repositories/cash-shift.repository.ts +++ b/src/modules/orders/repositories/cash-shift.repository.ts @@ -73,10 +73,16 @@ export class CashShiftRepository extends EntityRepository { }; } - findOpenShift(restId: string): Promise { - return this.findOne( - { restaurant: { id: restId }, status: CashShiftStatus.OPEN }, - { populate: ['admin'] }, - ); + findOpenShift(restId: string, adminId?: string): Promise { + const where: FilterQuery = { + restaurant: { id: restId }, + status: CashShiftStatus.OPEN, + }; + + if (adminId) { + where.admin = { id: adminId }; + } + + return this.findOne(where, { populate: ['admin'] }); } } diff --git a/src/modules/orders/repositories/order.repository.ts b/src/modules/orders/repositories/order.repository.ts index 8602e03..93fd2a8 100644 --- a/src/modules/orders/repositories/order.repository.ts +++ b/src/modules/orders/repositories/order.repository.ts @@ -124,58 +124,10 @@ export class OrderRepository extends EntityRepository { limit, offset, orderBy: { [orderBy]: order.toLowerCase() as 'asc' | 'desc' }, - populate: ['user', 'restaurant', 'deliveryMethod', 'paymentMethod', 'cashShift', 'items', 'items.food'] as never, + populate: ['user', 'restaurant', 'deliveryMethod', 'paymentMethod', 'cashShift', 'payments'] as never, }); - // Collect all (orderId, foodId) pairs for efficient review lookup - const orderFoodPairs: Array<{ orderId: string; foodId: string }> = []; - for (const order of data) { - for (const item of order.items.getItems()) { - if (item.food?.id) { - orderFoodPairs.push({ orderId: order.id, foodId: item.food.id }); - } - } - } - - // Fetch all relevant reviews in a single query - const reviewsMap: Map = new Map(); - if (orderFoodPairs.length > 0) { - const orderIds = [...new Set(orderFoodPairs.map(p => p.orderId))]; - const foodIds = [...new Set(orderFoodPairs.map(p => p.foodId))]; - - const reviews = await this.em.find( - Review, - { - order: { id: { $in: orderIds } }, - food: { id: { $in: foodIds } }, - }, - { - fields: ['id', 'order', 'food'], - populate: ['order', 'food'], - }, - ); - - // Create a map: key = `${orderId}-${foodId}`, value = reviewId - for (const review of reviews) { - const key = `${review.order.id}-${review.food.id}`; - reviewsMap.set(key, review.id); - } - } - - // Map reviewIds to foods - for (const order of data) { - for (const item of order.items.getItems()) { - if (item.food) { - const key = `${order.id}-${item.food.id}`; - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const food = item.food as any; - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - food.reviewId = reviewsMap.get(key) || null; - } - } - } - - const totalPages = Math.ceil(total / limit); + const totalPages = Math.ceil(total / limit); return { data, diff --git a/src/seeders/data/roles.data.ts b/src/seeders/data/roles.data.ts index 65d961b..d272a44 100644 --- a/src/seeders/data/roles.data.ts +++ b/src/seeders/data/roles.data.ts @@ -33,6 +33,9 @@ export const rolesData: RoleConfig[] = [ name: 'حسابدار ', isSystem: false, permissionFilter: (name: Permission) => - name === Permission.VIEW_REPORTS || name === Permission.MANAGE_PAYMENTS || name === Permission.MANAGE_ORDERS, + name === Permission.VIEW_REPORTS || + name === Permission.VIEW_ORDER_REPORTS || + name === Permission.MANAGE_PAYMENTS || + name === Permission.MANAGE_ORDERS, }, ];