init
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
import { Controller, Get, Post, Body, UseGuards, Res, Req, Patch, Param } from '@nestjs/common';
|
||||
import { PagerService } from '../providers/pager.service';
|
||||
import { CreatePagerDto } from '../dto/create-pager.dto';
|
||||
import { OptionalAuthGuard } from '../../auth/guards/optinalAuth.guard';
|
||||
import { RestId } from 'src/common/decorators/rest-id.decorator';
|
||||
import { UserId } from 'src/common/decorators/user-id.decorator';
|
||||
import { RestSlug } from 'src/common/decorators/rest-slug.decorator';
|
||||
import { ApiTags, ApiOperation, ApiBody, ApiHeader, ApiBearerAuth, ApiParam } from '@nestjs/swagger';
|
||||
import type { FastifyReply, FastifyRequest } from 'fastify';
|
||||
import { AdminAuthGuard } from '../../auth/guards/adminAuth.guard';
|
||||
import { AdminId } from 'src/common/decorators/admin-id.decorator';
|
||||
import { UpdatePagerStatusDto } from '../dto/update-pager.dto';
|
||||
import { PagerMessage } from 'src/common/enums/message.enum';
|
||||
import { Permissions } from 'src/common/decorators/permissions.decorator';
|
||||
import { Permission } from 'src/common/enums/permission.enum';
|
||||
import { API_HEADER_SLUG } from 'src/common/constants';
|
||||
|
||||
@ApiTags('pager')
|
||||
@Controller()
|
||||
export class PagerController {
|
||||
constructor(private readonly pagerService: PagerService) { }
|
||||
|
||||
@UseGuards(OptionalAuthGuard)
|
||||
@Post('public/pager')
|
||||
@ApiOperation({ summary: 'Create a new pager request' })
|
||||
@ApiHeader({ name: 'X-Slug', required: true, description: 'Restaurant slug identifier' })
|
||||
@ApiHeader({ name: 'Authorization', required: false, description: 'Bearer token (optional authentication)' })
|
||||
@ApiBody({ type: CreatePagerDto })
|
||||
async create(
|
||||
@Body() createPagerDto: CreatePagerDto,
|
||||
@Req() request: FastifyRequest,
|
||||
@Res() reply: FastifyReply,
|
||||
@RestSlug() slug: string,
|
||||
@RestId() restId?: string,
|
||||
@UserId() userId?: string,
|
||||
) {
|
||||
// Convert empty strings to undefined for cleaner validation
|
||||
const normalizedRestId = restId || undefined;
|
||||
const normalizedUserId = userId || undefined;
|
||||
const userCookieId = request.cookies['pagerId'] || undefined;
|
||||
|
||||
const pager = await this.pagerService.create(createPagerDto, {
|
||||
restId: normalizedRestId,
|
||||
userId: normalizedUserId,
|
||||
userCookieId,
|
||||
slug,
|
||||
});
|
||||
reply.setCookie('pagerId', pager.cookieId, {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
maxAge: 1000 * 60 * 60 * 24 * 1, // 1 days
|
||||
path: '/',
|
||||
});
|
||||
return reply.code(200).send({
|
||||
success: true,
|
||||
message: PagerMessage.CREATED_SUCCESS,
|
||||
data: null,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@UseGuards(OptionalAuthGuard)
|
||||
@Get('public/pager')
|
||||
@ApiOperation({ summary: 'Get all pager requests for the current session' })
|
||||
@ApiHeader(API_HEADER_SLUG)
|
||||
@ApiHeader({ name: 'Authorization', required: false, description: 'Bearer token (optional authentication)' })
|
||||
async findAll(@Req() request: FastifyRequest) {
|
||||
const cookieId = request.cookies['pagerId'] || undefined;
|
||||
if (!cookieId) {
|
||||
return [];
|
||||
}
|
||||
return this.pagerService.findAll(cookieId);
|
||||
}
|
||||
|
||||
/******************** Admin Routes **********************/
|
||||
|
||||
@UseGuards(AdminAuthGuard)
|
||||
@Permissions(Permission.MANAGE_PAGER)
|
||||
@ApiBearerAuth()
|
||||
@Get('admin/pager')
|
||||
@ApiOperation({ summary: 'Get all pager requests for the restaurant' })
|
||||
async findAllAdmin(@RestId() restId: string) {
|
||||
return this.pagerService.findAllByRestaurantId(restId);
|
||||
}
|
||||
|
||||
@UseGuards(AdminAuthGuard)
|
||||
@Permissions(Permission.MANAGE_PAGER)
|
||||
@ApiBearerAuth()
|
||||
@Patch('admin/pager/:pagerId/status')
|
||||
@ApiParam({ name: 'pagerId', required: true, type: String })
|
||||
@ApiOperation({ summary: 'Update the status of a pager request' })
|
||||
@ApiBody({ type: UpdatePagerStatusDto })
|
||||
async updateStatus(
|
||||
@Param('pagerId') pagerId: string,
|
||||
@RestId() restId: string,
|
||||
@AdminId() adminId: string,
|
||||
@Body() dto: UpdatePagerStatusDto,
|
||||
) {
|
||||
return this.pagerService.updateStatus(pagerId, restId, adminId, dto.status);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import type { ExecutionContext } from '@nestjs/common';
|
||||
import { createParamDecorator } from '@nestjs/common';
|
||||
import type { AuthenticatedSocket } from '../guards/ws-admin-auth.guard';
|
||||
|
||||
/**
|
||||
* Extract admin ID from authenticated WebSocket client
|
||||
* Use this decorator in WebSocket handlers to get the adminId from the authenticated admin
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* @SubscribeMessage('some:event')
|
||||
* handleEvent(@WsAdminId() adminId: string) {
|
||||
* // adminId is automatically extracted from authenticated admin
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export const WsAdminId = createParamDecorator((data: unknown, ctx: ExecutionContext): string => {
|
||||
const client = ctx.switchToWs().getClient<AuthenticatedSocket>();
|
||||
const adminId = client.adminId;
|
||||
|
||||
if (!adminId) {
|
||||
throw new Error('Admin ID not found. Ensure WsAdminAuthGuard is applied.');
|
||||
}
|
||||
|
||||
return adminId;
|
||||
});
|
||||
@@ -0,0 +1,26 @@
|
||||
import type { ExecutionContext } from '@nestjs/common';
|
||||
import { createParamDecorator } from '@nestjs/common';
|
||||
import type { AuthenticatedSocket } from '../guards/ws-admin-auth.guard';
|
||||
|
||||
/**
|
||||
* Extract restaurant ID from authenticated WebSocket client
|
||||
* Use this decorator in WebSocket handlers to get the restId from the authenticated admin
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* @SubscribeMessage('join:restaurant')
|
||||
* handleJoinRestaurant(@WsRestId() restId: string) {
|
||||
* // restId is automatically extracted from authenticated admin
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export const WsRestId = createParamDecorator((data: unknown, ctx: ExecutionContext): string => {
|
||||
const client = ctx.switchToWs().getClient<AuthenticatedSocket>();
|
||||
const restId = client.restId;
|
||||
|
||||
if (!restId) {
|
||||
throw new Error('Restaurant ID not found. Ensure WsAdminAuthGuard is applied.');
|
||||
}
|
||||
|
||||
return restId;
|
||||
});
|
||||
@@ -0,0 +1,15 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
|
||||
import { IsNotEmpty, IsOptional, IsString } from 'class-validator';
|
||||
|
||||
export class CreatePagerDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@ApiProperty({ description: 'Table number' })
|
||||
tableNumber!: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({ description: 'Message' })
|
||||
message?: string;
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
import type { PagerStatus } from '../interface/pager';
|
||||
|
||||
/**
|
||||
* Socket connection configuration for admin pager module
|
||||
* Used by frontend to establish WebSocket connection
|
||||
*/
|
||||
export interface PagerSocketConnectionConfig {
|
||||
/**
|
||||
* WebSocket server URL
|
||||
* Example: 'http://localhost:3000' or 'https://api.example.com'
|
||||
*/
|
||||
serverUrl: string;
|
||||
|
||||
/**
|
||||
* Socket namespace (default: '/pager')
|
||||
* The gateway is configured to use '/pager' namespace
|
||||
*/
|
||||
namespace?: string;
|
||||
|
||||
/**
|
||||
* JWT authentication token for admin
|
||||
* Token should contain adminId and restId in payload
|
||||
* Can be passed via auth.token, auth.authorization, query.token, or headers.authorization
|
||||
* Example: { auth: { token: 'your-jwt-token' } }
|
||||
*/
|
||||
token: string;
|
||||
|
||||
/**
|
||||
* Optional: Additional connection options
|
||||
*/
|
||||
options?: {
|
||||
/**
|
||||
* Enable auto-reconnection
|
||||
* @default true
|
||||
*/
|
||||
autoConnect?: boolean;
|
||||
|
||||
/**
|
||||
* Connection timeout in milliseconds
|
||||
* @default 5000
|
||||
*/
|
||||
timeout?: number;
|
||||
|
||||
/**
|
||||
* Enable transports (websocket, polling)
|
||||
* @default ['websocket', 'polling']
|
||||
*/
|
||||
transports?: ('websocket' | 'polling')[];
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Socket event names for pager module
|
||||
*/
|
||||
export enum PagerSocketEvents {
|
||||
// Client to Server Events
|
||||
JOIN_RESTAURANT = 'join:restaurant',
|
||||
LEAVE_ROOM = 'leave:room',
|
||||
GET_PAGERS = 'get:pagers',
|
||||
UPDATE_PAGER_STATUS = 'update:pager:status',
|
||||
|
||||
// Server to Client Events
|
||||
JOINED = 'joined',
|
||||
LEFT = 'left',
|
||||
PAGER_CREATED = 'pager:created',
|
||||
PAGER_STATUS_UPDATED = 'pager:status:updated',
|
||||
PAGERS_LIST = 'pagers:list',
|
||||
PAGER_STATUS_UPDATED_RESPONSE = 'pager:status:updated:response',
|
||||
ERROR = 'error',
|
||||
CONNECT = 'connect',
|
||||
DISCONNECT = 'disconnect',
|
||||
}
|
||||
|
||||
/**
|
||||
* Payload for joining restaurant room
|
||||
* Note: restId is automatically extracted from JWT token, no need to send it
|
||||
*/
|
||||
export type JoinRestaurantPayload = Record<string, never>;
|
||||
|
||||
/**
|
||||
* Payload for leaving a room
|
||||
*/
|
||||
export interface LeaveRoomPayload {
|
||||
room: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Payload for getting pagers list
|
||||
* Note: restId is automatically extracted from JWT token, no need to send it
|
||||
*/
|
||||
export type GetPagersPayload = Record<string, never>;
|
||||
|
||||
/**
|
||||
* Payload for updating pager status
|
||||
* Note: restId and adminId are automatically extracted from JWT token
|
||||
*/
|
||||
export interface UpdatePagerStatusPayload {
|
||||
pagerId: string;
|
||||
status: PagerStatus;
|
||||
}
|
||||
|
||||
/**
|
||||
* Response when successfully joined a room
|
||||
*/
|
||||
export interface JoinedResponse {
|
||||
room: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Response when successfully left a room
|
||||
*/
|
||||
export interface LeftResponse {
|
||||
room: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* User information in pager response
|
||||
*/
|
||||
export interface PagerUserInfo {
|
||||
id: string;
|
||||
firstName: string;
|
||||
lastName?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Staff information in pager response
|
||||
*/
|
||||
export interface PagerStaffInfo {
|
||||
id: string;
|
||||
firstName: string;
|
||||
lastName?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pager data structure emitted by socket events
|
||||
*/
|
||||
export interface PagerSocketData {
|
||||
id: string;
|
||||
tableNumber: string;
|
||||
message?: string;
|
||||
status: PagerStatus;
|
||||
cookieId?: string;
|
||||
createdAt?: Date | string;
|
||||
updatedAt?: Date | string;
|
||||
user?: PagerUserInfo | null;
|
||||
staff?: PagerStaffInfo | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Payload for pager:created event
|
||||
*/
|
||||
export interface PagerCreatedPayload {
|
||||
pager: PagerSocketData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Payload for pager:status:updated event
|
||||
*/
|
||||
export interface PagerStatusUpdatedPayload {
|
||||
pager: PagerSocketData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Payload for pagers:list event
|
||||
*/
|
||||
export interface PagersListPayload {
|
||||
pagers: PagerSocketData[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Payload for pager:status:updated:response event (response to update request)
|
||||
*/
|
||||
export interface PagerStatusUpdatedResponsePayload {
|
||||
pager: PagerSocketData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Error response from socket
|
||||
*/
|
||||
export interface SocketErrorResponse {
|
||||
message: string;
|
||||
code?: string;
|
||||
details?: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* Complete socket event map for type safety
|
||||
*/
|
||||
export interface PagerSocketEventMap {
|
||||
// Client emits
|
||||
[PagerSocketEvents.JOIN_RESTAURANT]: (payload?: JoinRestaurantPayload) => void;
|
||||
[PagerSocketEvents.LEAVE_ROOM]: (payload: LeaveRoomPayload) => void;
|
||||
[PagerSocketEvents.GET_PAGERS]: (payload?: GetPagersPayload) => void;
|
||||
[PagerSocketEvents.UPDATE_PAGER_STATUS]: (payload: UpdatePagerStatusPayload) => void;
|
||||
|
||||
// Server emits
|
||||
[PagerSocketEvents.JOINED]: (response: JoinedResponse) => void;
|
||||
[PagerSocketEvents.LEFT]: (response: LeftResponse) => void;
|
||||
[PagerSocketEvents.PAGER_CREATED]: (payload: PagerCreatedPayload) => void;
|
||||
[PagerSocketEvents.PAGER_STATUS_UPDATED]: (payload: PagerStatusUpdatedPayload) => void;
|
||||
[PagerSocketEvents.PAGERS_LIST]: (payload: PagersListPayload) => void;
|
||||
[PagerSocketEvents.PAGER_STATUS_UPDATED_RESPONSE]: (payload: PagerStatusUpdatedResponsePayload) => void;
|
||||
[PagerSocketEvents.ERROR]: (error: SocketErrorResponse) => void;
|
||||
[PagerSocketEvents.CONNECT]: () => void;
|
||||
[PagerSocketEvents.DISCONNECT]: (reason: string) => void;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsEnum, IsNotEmpty } from 'class-validator';
|
||||
import { PagerStatus } from '../interface/pager';
|
||||
|
||||
export class UpdatePagerStatusDto {
|
||||
@ApiProperty({ description: 'Status of the pager', enum: PagerStatus })
|
||||
@IsEnum(PagerStatus)
|
||||
@IsNotEmpty()
|
||||
status!: PagerStatus;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { Entity, ManyToOne, Property, Enum } from '@mikro-orm/core';
|
||||
import { BaseEntity } from '../../../common/entities/base.entity';
|
||||
import { User } from '../../users/entities/user.entity';
|
||||
import { Restaurant } from '../../restaurants/entities/restaurant.entity';
|
||||
import { PagerStatus } from '../interface/pager';
|
||||
import { Admin } from 'src/modules/admin/entities/admin.entity';
|
||||
|
||||
@Entity({ tableName: 'pagers' })
|
||||
export class Pager extends BaseEntity {
|
||||
@Property({ type: 'string' })
|
||||
cookieId!: string;
|
||||
|
||||
@ManyToOne(() => Restaurant)
|
||||
restaurant!: Restaurant;
|
||||
|
||||
@Property({ type: 'string' })
|
||||
tableNumber!: string;
|
||||
|
||||
@ManyToOne(() => User, { nullable: true })
|
||||
user?: User;
|
||||
|
||||
@ManyToOne(() => Admin, { nullable: true })
|
||||
staff?: Admin;
|
||||
|
||||
@Property({ type: 'string', nullable: true })
|
||||
message?: string;
|
||||
|
||||
@Enum(() => PagerStatus)
|
||||
status: PagerStatus = PagerStatus.Pending;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export class PagerCreatedEvent {
|
||||
constructor(
|
||||
public readonly restaurantId: string,
|
||||
public readonly tableNumber: string,
|
||||
) {}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
import { CanActivate, ExecutionContext, Injectable, Logger, Inject } from '@nestjs/common';
|
||||
import { WsException } from '@nestjs/websockets';
|
||||
import { Socket } from 'socket.io';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { IAdminTokenPayload } from '../../auth/interfaces/IToken-payload';
|
||||
|
||||
export interface AuthenticatedSocket extends Socket {
|
||||
adminId?: string;
|
||||
restId?: string;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class WsAdminAuthGuard implements CanActivate {
|
||||
private readonly logger = new Logger(WsAdminAuthGuard.name);
|
||||
|
||||
constructor(
|
||||
@Inject(JwtService)
|
||||
private readonly jwtService: JwtService,
|
||||
@Inject(ConfigService)
|
||||
private readonly configService: ConfigService,
|
||||
) {}
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const client: Socket = context.switchToWs().getClient<Socket>();
|
||||
|
||||
try {
|
||||
const token = this.extractToken(client);
|
||||
|
||||
if (!token) {
|
||||
this.logger.warn('No token provided in WebSocket connection', {
|
||||
socketId: client.id,
|
||||
auth: client.handshake.auth,
|
||||
query: client.handshake.query,
|
||||
});
|
||||
throw new WsException('Authentication required');
|
||||
}
|
||||
|
||||
const secret = this.configService.getOrThrow<string>('JWT_SECRET');
|
||||
const payload = await this.jwtService.verifyAsync<IAdminTokenPayload>(token, {
|
||||
secret,
|
||||
});
|
||||
|
||||
if (!payload.adminId || !payload.restId) {
|
||||
this.logger.error('Invalid token payload structure', payload);
|
||||
throw new WsException('Invalid token payload');
|
||||
}
|
||||
|
||||
// Attach admin info to socket
|
||||
(client as AuthenticatedSocket).adminId = payload.adminId;
|
||||
(client as AuthenticatedSocket).restId = payload.restId;
|
||||
|
||||
this.logger.log(`Admin authenticated via WebSocket: ${payload.adminId}, restId: ${payload.restId}`);
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
if (error instanceof WsException) {
|
||||
throw error;
|
||||
}
|
||||
this.logger.error('WebSocket authentication error', {
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
socketId: client.id,
|
||||
});
|
||||
throw new WsException('Authentication failed');
|
||||
}
|
||||
}
|
||||
|
||||
private extractToken(client: Socket): string | undefined {
|
||||
// Try to get token from handshake auth (recommended for socket.io)
|
||||
const auth = client.handshake.auth;
|
||||
const authToken = (auth?.token as string | undefined) || (auth?.authorization as string | undefined);
|
||||
|
||||
if (authToken) {
|
||||
// If it's "Bearer <token>", extract just the token
|
||||
if (typeof authToken === 'string' && authToken.startsWith('Bearer ')) {
|
||||
return authToken.substring(7);
|
||||
}
|
||||
return authToken;
|
||||
}
|
||||
|
||||
// Try to get from query parameters (fallback)
|
||||
const queryToken = client.handshake.query?.token || client.handshake.query?.authorization;
|
||||
|
||||
if (queryToken) {
|
||||
if (typeof queryToken === 'string' && queryToken.startsWith('Bearer ')) {
|
||||
return queryToken.substring(7);
|
||||
}
|
||||
return queryToken as string;
|
||||
}
|
||||
|
||||
// Try to get from headers (if available)
|
||||
const headers = client.handshake.headers;
|
||||
const authHeader = headers.authorization || headers['authorization'];
|
||||
|
||||
if (authHeader && typeof authHeader === 'string') {
|
||||
const [type, token] = authHeader.split(' ');
|
||||
if (type?.toLowerCase() === 'bearer' && token) {
|
||||
return token;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export enum PagerStatus {
|
||||
Pending = 'pending',
|
||||
Acknowledged = 'acknowledged',
|
||||
Rejected = 'rejected',
|
||||
Completed = 'completed',
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { OnEvent } from '@nestjs/event-emitter';
|
||||
import { AdminRepository } from 'src/modules/admin/repositories/admin.repository';
|
||||
import { PagerCreatedEvent } from '../events/pager.events';
|
||||
import { Permission } from 'src/common/enums/permission.enum';
|
||||
import { NotifTitleEnum } from 'src/modules/notifications/interfaces/notification.interface';
|
||||
import { NotificationService } from 'src/modules/notifications/services/notification.service';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
|
||||
@Injectable()
|
||||
export class PagerListeners {
|
||||
private readonly logger = new Logger(PagerListeners.name);
|
||||
private readonly smsPatternPagerCreated: string
|
||||
constructor(
|
||||
private readonly adminService: AdminRepository,
|
||||
private readonly notificationService: NotificationService,
|
||||
private readonly configService: ConfigService,
|
||||
) {
|
||||
this.smsPatternPagerCreated = this.configService.get<string>('SMS_PATTERN_PAGER_CREATED') ?? '123';
|
||||
}
|
||||
|
||||
@OnEvent(PagerCreatedEvent.name)
|
||||
async handlePagerCreated(event: PagerCreatedEvent) {
|
||||
try {
|
||||
this.logger.log(`Pager created event received: ${event.restaurantId}`);
|
||||
// get admnin os restuaraant that have pager permissuins
|
||||
const admins = await this.adminService.findAdminsWithPermission(event.restaurantId, Permission.MANAGE_PAGER);
|
||||
const recipients = admins.map(admin => ({
|
||||
adminId: admin.id,
|
||||
}));
|
||||
await this.notificationService.sendNotification({
|
||||
restaurantId: event.restaurantId,
|
||||
message: {
|
||||
title: NotifTitleEnum.PAGER_CREATED,
|
||||
content: `میز شماره ${event.tableNumber} پیج جدیدی انجام داد`,
|
||||
sms: {
|
||||
templateId: this.smsPatternPagerCreated,
|
||||
parameters: {
|
||||
tableNumber: event.tableNumber.toString(),
|
||||
},
|
||||
},
|
||||
pushNotif: {
|
||||
title: ` پیج جدید`,
|
||||
content: `میز شماره ${event.tableNumber} پیج جدیدی انجام داد`,
|
||||
icon: `/assets/images/logo.png`,
|
||||
action: {
|
||||
type: NotifTitleEnum.PAGER_CREATED,
|
||||
url: `/restaurants/${event.restaurantId}/pagers`,
|
||||
},
|
||||
},
|
||||
},
|
||||
recipients,
|
||||
metadata: {
|
||||
priority: 1,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Failed to send notification for pager created event: ${event.restaurantId}`,
|
||||
error instanceof Error ? error.stack : String(error),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { PagerService } from './providers/pager.service';
|
||||
import { PagerController } from './controllers/pager.controller';
|
||||
import { MikroOrmModule } from '@mikro-orm/nestjs';
|
||||
import { Pager } from './entities/pager.entity';
|
||||
import { JwtModule } from '@nestjs/jwt';
|
||||
import { RolesModule } from '../roles/roles.module';
|
||||
import { PagerListeners } from './listeners/notification.listeners';
|
||||
import { AdminModule } from '../admin/admin.module';
|
||||
import { NotificationsModule } from '../notifications/notifications.module';
|
||||
|
||||
@Module({
|
||||
imports: [MikroOrmModule.forFeature([Pager]), JwtModule, RolesModule, AdminModule, NotificationsModule],
|
||||
controllers: [PagerController],
|
||||
providers: [PagerService, PagerListeners],
|
||||
})
|
||||
export class PagerModule {}
|
||||
@@ -0,0 +1,115 @@
|
||||
import { BadRequestException, Injectable, NotFoundException, Inject, forwardRef } from '@nestjs/common';
|
||||
import { CreatePagerDto } from '../dto/create-pager.dto';
|
||||
import { User } from '../../users/entities/user.entity';
|
||||
import { EntityManager } from '@mikro-orm/postgresql';
|
||||
import { Restaurant } from '../../restaurants/entities/restaurant.entity';
|
||||
import { Pager } from '../entities/pager.entity';
|
||||
import { PagerStatus } from '../interface/pager';
|
||||
import { ulid } from 'ulid';
|
||||
import { Admin } from '../../admin/entities/admin.entity';
|
||||
import { PagerCreatedEvent } from '../events/pager.events';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
|
||||
@Injectable()
|
||||
export class PagerService {
|
||||
constructor(
|
||||
private readonly em: EntityManager,
|
||||
// @Inject(forwardRef(() => PagerGateway))
|
||||
// private readonly pagerGateway: PagerGateway,
|
||||
private readonly eventEmitter: EventEmitter2,
|
||||
) {}
|
||||
|
||||
async create(
|
||||
createPagerDto: CreatePagerDto,
|
||||
params: { restId?: string; userId?: string; slug: string; userCookieId?: string },
|
||||
) {
|
||||
const { restId, userId, slug, userCookieId } = params;
|
||||
let restaurant: Restaurant | null = null;
|
||||
let user: User | null = null;
|
||||
if (!restId && !userId && !slug) {
|
||||
throw new BadRequestException('Invalid parameters');
|
||||
}
|
||||
if (restId) {
|
||||
restaurant = await this.em.findOne(Restaurant, { id: restId });
|
||||
if (!restaurant) {
|
||||
throw new NotFoundException('Restaurant not found');
|
||||
}
|
||||
}
|
||||
if (userId) {
|
||||
user = await this.em.findOne(User, { id: userId });
|
||||
if (!user) {
|
||||
throw new NotFoundException('User not found');
|
||||
}
|
||||
}
|
||||
if (!restaurant && slug) {
|
||||
restaurant = await this.em.findOne(Restaurant, { slug });
|
||||
if (!restaurant) {
|
||||
throw new NotFoundException('Restaurant not found');
|
||||
}
|
||||
}
|
||||
const cookieId = userCookieId || ulid().toString();
|
||||
// const existingPager = await this.em.findOne(Pager, {
|
||||
// restaurant: { id: restaurant!.id },
|
||||
// status: PagerStatus.Pending,
|
||||
// cookieId,
|
||||
// });
|
||||
// if (existingPager) {
|
||||
// throw new BadRequestException('Pager already exists');
|
||||
// }
|
||||
|
||||
const pager = this.em.create(Pager, {
|
||||
...createPagerDto,
|
||||
restaurant: restaurant!,
|
||||
user,
|
||||
cookieId: ulid().toString(),
|
||||
status: PagerStatus.Pending,
|
||||
});
|
||||
|
||||
await this.em.persistAndFlush(pager);
|
||||
|
||||
// Populate relations before emitting
|
||||
await this.em.populate(pager, ['restaurant']);
|
||||
// Emit socket event for new pager
|
||||
this.eventEmitter.emit(PagerCreatedEvent.name, new PagerCreatedEvent(pager.restaurant.id, pager.tableNumber));
|
||||
|
||||
return pager;
|
||||
}
|
||||
|
||||
async findAll(cookieId: string) {
|
||||
const pagers = await this.em.find(Pager, { cookieId }, { populate: ['user'] });
|
||||
return pagers;
|
||||
}
|
||||
|
||||
async findAllByRestaurantId(restId: string) {
|
||||
const pagers = await this.em.find(Pager, { restaurant: { id: restId } }, { populate: ['user'] });
|
||||
return pagers;
|
||||
}
|
||||
|
||||
async updateStatus(pagerId: string, restId: string, staffId: string, status: PagerStatus) {
|
||||
const pager = await this.em.findOne(
|
||||
Pager,
|
||||
{ id: pagerId, restaurant: { id: restId } },
|
||||
{ populate: ['restaurant'] },
|
||||
);
|
||||
if (!pager) {
|
||||
throw new NotFoundException('Pager not found *');
|
||||
}
|
||||
const staff = await this.em.findOne(Admin, { id: staffId });
|
||||
if (!staff) {
|
||||
throw new NotFoundException('Staff not found');
|
||||
}
|
||||
pager.status = status;
|
||||
if (status === PagerStatus.Acknowledged) {
|
||||
pager.staff = staff;
|
||||
}
|
||||
await this.em.persistAndFlush(pager);
|
||||
|
||||
// Populate relations before emitting
|
||||
await this.em.populate(pager, ['restaurant', 'staff', 'user']);
|
||||
|
||||
// Emit socket event for status update
|
||||
// this.pagerGateway.emitPagerStatusUpdated(pager);
|
||||
|
||||
return pager;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user