import { WebSocketGateway, WebSocketServer, SubscribeMessage, OnGatewayConnection, OnGatewayDisconnect, MessageBody, ConnectedSocket, } from '@nestjs/websockets'; import { Server, Socket } from 'socket.io'; import { Logger, UseGuards, Inject, forwardRef } from '@nestjs/common'; import { Pager } from './entities/pager.entity'; import { WsAdminAuthGuard, type AuthenticatedSocket } from './guards/ws-admin-auth.guard'; import { WsRestId } from './decorators/ws-rest-id.decorator'; import { PagerService } from './providers/pager.service'; import { WsAdminId } from './decorators/ws-admin-id.decorator'; import { PagerStatus } from './interface/pager'; @WebSocketGateway({ cors: { origin: true, credentials: true, }, namespace: '/pager', }) export class PagerGateway implements OnGatewayConnection, OnGatewayDisconnect { @WebSocketServer() server!: Server; private readonly logger = new Logger(PagerGateway.name); constructor( @Inject(forwardRef(() => PagerService)) private readonly pagerService: PagerService, ) {} handleConnection(client: Socket) { this.logger.log(`Client connected: ${client.id}`); } handleDisconnect(client: Socket) { this.logger.log(`Client disconnected: ${client.id}`); } /** * Join a room for a specific restaurant (admin/staff) * Requires admin authentication via JWT token * Restaurant ID is extracted from the authenticated admin's token * Room format: `restaurant:${restId}` */ @UseGuards(WsAdminAuthGuard) @SubscribeMessage('join:restaurant') handleJoinRestaurant(@ConnectedSocket() client: AuthenticatedSocket, @WsRestId() restId: string) { const room = `restaurant:${restId}`; void client.join(room); this.logger.log(`Admin ${client.adminId} (Client ${client.id}) joined room: ${room}`); void client.emit('joined', { room, message: 'Successfully joined restaurant room' }); } /** * Join a room for a specific cookie/session (public user) * Room format: `cookie:${cookieId}` */ @SubscribeMessage('join:session') handleJoinSession(@ConnectedSocket() client: Socket, @MessageBody() data: { cookieId: string }) { if (!data.cookieId) { void client.emit('error', { message: 'Cookie ID is required' }); return; } const room = `cookie:${data.cookieId}`; void client.join(room); this.logger.log(`Client ${client.id} joined room: ${room}`); void client.emit('joined', { room, message: 'Successfully joined session room' }); } /** * Leave a room */ @SubscribeMessage('leave:room') handleLeaveRoom(@ConnectedSocket() client: Socket, @MessageBody() data: { room: string }) { if (data.room) { void client.leave(data.room); this.logger.log(`Client ${client.id} left room: ${data.room}`); void client.emit('left', { room: data.room, message: 'Successfully left room' }); } } /** * Get list of pagers for the restaurant (admin only) * Requires admin authentication via JWT token * Restaurant ID is extracted from the authenticated admin's token */ @UseGuards(WsAdminAuthGuard) @SubscribeMessage('get:pagers') async handleGetPagers(@ConnectedSocket() client: AuthenticatedSocket, @WsRestId() restId: string) { try { const pagers = await this.pagerService.findAllByRestaurantId(restId); const pagersData = pagers.map(pager => ({ id: pager.id, tableNumber: pager.tableNumber, message: pager.message, status: pager.status, cookieId: pager.cookieId, createdAt: pager.createdAt, updatedAt: pager.updatedAt, user: pager.user ? { id: pager.user.id, firstName: pager.user.firstName, lastName: pager.user.lastName, } : null, staff: pager.staff ? { id: pager.staff.id, firstName: pager.staff.firstName, lastName: pager.staff.lastName, } : null, })); void client.emit('pagers:list', { pagers: pagersData }); this.logger.log(`Admin ${client.adminId} requested pagers list for restaurant ${restId}`); } catch (error) { this.logger.error(`Error getting pagers list: ${error instanceof Error ? error.message : 'Unknown error'}`); void client.emit('error', { message: 'Failed to get pagers list', code: 'GET_PAGERS_ERROR', details: error instanceof Error ? error.message : 'Unknown error', }); } } /** * Update pager status (admin only) * Requires admin authentication via JWT token * Restaurant ID and Admin ID are extracted from the authenticated admin's token */ @UseGuards(WsAdminAuthGuard) @SubscribeMessage('update:pager:status') async handleUpdatePagerStatus( @ConnectedSocket() client: AuthenticatedSocket, @WsRestId() restId: string, @WsAdminId() adminId: string, @MessageBody() data: { pagerId: string; status: PagerStatus }, ) { try { if (!data.pagerId || !data.status) { void client.emit('error', { message: 'Pager ID and status are required', code: 'INVALID_PAYLOAD', }); return; } const pager = await this.pagerService.updateStatus(data.pagerId, restId, adminId, data.status); const pagerData = { id: pager.id, tableNumber: pager.tableNumber, message: pager.message, status: pager.status, updatedAt: pager.updatedAt, staff: pager.staff ? { id: pager.staff.id, firstName: pager.staff.firstName, lastName: pager.staff.lastName, } : null, }; void client.emit('pager:status:updated:response', { pager: pagerData }); this.logger.log(`Admin ${adminId} updated pager ${data.pagerId} status to ${data.status}`); } catch (error) { this.logger.error(`Error updating pager status: ${error instanceof Error ? error.message : 'Unknown error'}`); void client.emit('error', { message: error instanceof Error ? error.message : 'Failed to update pager status', code: 'UPDATE_PAGER_STATUS_ERROR', details: error instanceof Error ? error.message : 'Unknown error', }); } } /** * Emit pager created event to restaurant room */ emitPagerCreated(pager: Pager) { const room = `restaurant:${pager.restaurant.id}`; this.server.to(room).emit('pager:created', { pager: { id: pager.id, tableNumber: pager.tableNumber, message: pager.message, status: pager.status, createdAt: pager.createdAt, user: pager.user ? { id: pager.user.id, firstName: pager.user.firstName, lastName: pager.user.lastName, } : null, }, }); this.logger.log(`Emitted pager:created to room: ${room}`); } /** * Emit pager status updated event to both restaurant and cookie rooms */ emitPagerStatusUpdated(pager: Pager) { const restaurantRoom = `restaurant:${pager.restaurant.id}`; const cookieRoom = `cookie:${pager.cookieId}`; const pagerData = { id: pager.id, tableNumber: pager.tableNumber, message: pager.message, status: pager.status, updatedAt: pager.updatedAt, staff: pager.staff ? { id: pager.staff.id, firstName: pager.staff.firstName, lastName: pager.staff.lastName, } : null, }; // Emit to restaurant room (admin/staff) this.server.to(restaurantRoom).emit('pager:status:updated', { pager: pagerData, }); this.logger.log(`Emitted pager:status:updated to room: ${restaurantRoom}`); // Emit to cookie room (public user) this.server.to(cookieRoom).emit('pager:status:updated', { pager: pagerData, }); this.logger.log(`Emitted pager:status:updated to room: ${cookieRoom}`); } }