@@ -792,6 +792,7 @@ export const enum CashShiftMessage {
|
||||
NOT_FOUND = 'شیفت صندوق یافت نشد',
|
||||
ALREADY_OPEN = 'یک شیفت صندوق باز از قبل وجود دارد',
|
||||
ALREADY_CLOSED = 'این شیفت صندوق قبلا بسته شده است',
|
||||
ACCESS_DENIED = 'شما به این شیفت صندوق دسترسی ندارید',
|
||||
ADMIN_NOT_FOUND = 'مدیر یافت نشد',
|
||||
RESTAURANT_NOT_FOUND = 'رستوران یافت نشد',
|
||||
}
|
||||
|
||||
@@ -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, string> = {
|
||||
[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]: 'مشاهده گزارشات',
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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<CashShift> {
|
||||
@@ -54,7 +56,12 @@ export class CashShiftsService {
|
||||
return shift;
|
||||
}
|
||||
|
||||
async updateShift(shiftId: string, restaurantId: string, dto: UpdateCashShiftDto): Promise<CashShift> {
|
||||
async closeShift(
|
||||
shiftId: string,
|
||||
restaurantId: string,
|
||||
adminId: string,
|
||||
dto: CloseCashShiftDto,
|
||||
): Promise<CashShift> {
|
||||
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<CashShift> {
|
||||
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<CashShiftSummary> {
|
||||
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<CashShiftSummary> {
|
||||
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<CashShiftSummary | null> {
|
||||
const shift = await this.cashShiftRepository.findOpenShift(restaurantId);
|
||||
async calculateCurrentShiftSummary(
|
||||
restaurantId: string,
|
||||
adminId: string,
|
||||
): Promise<CashShiftSummary | null> {
|
||||
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<PaginatedResult<CashShift>> {
|
||||
return this.cashShiftRepository.findAllPaginated(restaurantId, dto);
|
||||
async findAll(
|
||||
restaurantId: string,
|
||||
adminId: string,
|
||||
dto: FindCashShiftsDto,
|
||||
): Promise<PaginatedResult<CashShift>> {
|
||||
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<CashShift> {
|
||||
async findOne(shiftId: string, restaurantId: string, adminId: string): Promise<CashShift> {
|
||||
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<CashShift | null> {
|
||||
return this.cashShiftRepository.findOpenShift(restaurantId);
|
||||
async getCurrentOpenShift(restaurantId: string, adminId: string): Promise<CashShift | null> {
|
||||
const canViewAll = await this.canViewAllShifts(adminId, restaurantId);
|
||||
return this.cashShiftRepository.findOpenShift(restaurantId, canViewAll ? undefined : adminId);
|
||||
}
|
||||
|
||||
async assignOrderToOpenShift(order: Order, em: EntityManager = this.em): Promise<void> {
|
||||
@@ -185,4 +181,25 @@ export class CashShiftsService {
|
||||
{ populate: ['payments'] },
|
||||
);
|
||||
}
|
||||
|
||||
private async canViewAllShifts(adminId: string, restaurantId: string): Promise<boolean> {
|
||||
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<void> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,10 +73,16 @@ export class CashShiftRepository extends EntityRepository<CashShift> {
|
||||
};
|
||||
}
|
||||
|
||||
findOpenShift(restId: string): Promise<CashShift | null> {
|
||||
return this.findOne(
|
||||
{ restaurant: { id: restId }, status: CashShiftStatus.OPEN },
|
||||
{ populate: ['admin'] },
|
||||
);
|
||||
findOpenShift(restId: string, adminId?: string): Promise<CashShift | null> {
|
||||
const where: FilterQuery<CashShift> = {
|
||||
restaurant: { id: restId },
|
||||
status: CashShiftStatus.OPEN,
|
||||
};
|
||||
|
||||
if (adminId) {
|
||||
where.admin = { id: adminId };
|
||||
}
|
||||
|
||||
return this.findOne(where, { populate: ['admin'] });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,58 +124,10 @@ export class OrderRepository extends EntityRepository<Order> {
|
||||
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<string, string> = 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,
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user