feat: implement support chat functionality with new module, service, controller, DTOs, and schema updates.
This commit is contained in:
@@ -75,6 +75,11 @@ enum MessageStatus {
|
|||||||
READ
|
READ
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum SupportChatStatus {
|
||||||
|
OPEN
|
||||||
|
CLOSED
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// ===========================================
|
// ===========================================
|
||||||
// USER - Authentication Only
|
// USER - Authentication Only
|
||||||
@@ -135,6 +140,9 @@ model User {
|
|||||||
// Notifications
|
// Notifications
|
||||||
notifications Notification[]
|
notifications Notification[]
|
||||||
|
|
||||||
|
// Support Chat
|
||||||
|
supportChats SupportChat[]
|
||||||
|
|
||||||
@@index([email])
|
@@index([email])
|
||||||
@@index([role])
|
@@index([role])
|
||||||
@@index([status])
|
@@index([status])
|
||||||
@@ -572,3 +580,40 @@ model Testimonial {
|
|||||||
@@index([agentProfileId])
|
@@index([agentProfileId])
|
||||||
@@map("testimonials")
|
@@map("testimonials")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===========================================
|
||||||
|
// SUPPORT CHAT SYSTEM
|
||||||
|
// ===========================================
|
||||||
|
|
||||||
|
model SupportChat {
|
||||||
|
id String @id @default(uuid())
|
||||||
|
userId String
|
||||||
|
status SupportChatStatus @default(OPEN)
|
||||||
|
lastMessageAt DateTime?
|
||||||
|
lastMessageText String? @db.VarChar(255)
|
||||||
|
userUnreadCount Int @default(0)
|
||||||
|
adminUnreadCount Int @default(0)
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
messages SupportMessage[]
|
||||||
|
|
||||||
|
@@index([userId])
|
||||||
|
@@index([status])
|
||||||
|
@@map("support_chats")
|
||||||
|
}
|
||||||
|
|
||||||
|
model SupportMessage {
|
||||||
|
id String @id @default(uuid())
|
||||||
|
chatId String
|
||||||
|
senderId String
|
||||||
|
senderRole String // "USER" or "ADMIN"
|
||||||
|
content String @db.Text
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
|
||||||
|
chat SupportChat @relation(fields: [chatId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
|
@@index([chatId])
|
||||||
|
@@map("support_messages")
|
||||||
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import { MessagesModule } from './messages';
|
|||||||
import { CmsModule } from './cms';
|
import { CmsModule } from './cms';
|
||||||
import { NotificationsModule } from './notifications';
|
import { NotificationsModule } from './notifications';
|
||||||
import { TestimonialsModule } from './testimonials';
|
import { TestimonialsModule } from './testimonials';
|
||||||
|
import { SupportChatModule } from './support-chat';
|
||||||
import { AppController } from './app.controller';
|
import { AppController } from './app.controller';
|
||||||
import { AppService } from './app.service';
|
import { AppService } from './app.service';
|
||||||
|
|
||||||
@@ -61,6 +62,7 @@ import { AppService } from './app.service';
|
|||||||
CmsModule,
|
CmsModule,
|
||||||
NotificationsModule,
|
NotificationsModule,
|
||||||
TestimonialsModule,
|
TestimonialsModule,
|
||||||
|
SupportChatModule,
|
||||||
],
|
],
|
||||||
controllers: [AppController],
|
controllers: [AppController],
|
||||||
providers: [
|
providers: [
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { Server, Socket } from 'socket.io';
|
|||||||
import { JwtService } from '@nestjs/jwt';
|
import { JwtService } from '@nestjs/jwt';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import { MessagesService } from './messages.service';
|
import { MessagesService } from './messages.service';
|
||||||
|
import { SupportChatService } from '../support-chat/support-chat.service';
|
||||||
import { CreateMessageDto } from './dto';
|
import { CreateMessageDto } from './dto';
|
||||||
import { Logger } from '@nestjs/common';
|
import { Logger } from '@nestjs/common';
|
||||||
|
|
||||||
@@ -35,6 +36,7 @@ export class MessagesGateway implements OnGatewayConnection, OnGatewayDisconnect
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly messagesService: MessagesService,
|
private readonly messagesService: MessagesService,
|
||||||
|
private readonly supportChatService: SupportChatService,
|
||||||
private readonly jwtService: JwtService,
|
private readonly jwtService: JwtService,
|
||||||
private readonly configService: ConfigService,
|
private readonly configService: ConfigService,
|
||||||
) {}
|
) {}
|
||||||
@@ -266,6 +268,103 @@ export class MessagesGateway implements OnGatewayConnection, OnGatewayDisconnect
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// SUPPORT CHAT EVENTS
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Join a support chat room
|
||||||
|
*/
|
||||||
|
@SubscribeMessage('support_join')
|
||||||
|
async handleSupportJoin(
|
||||||
|
@ConnectedSocket() client: AuthenticatedSocket,
|
||||||
|
@MessageBody() data: { chatId: string },
|
||||||
|
) {
|
||||||
|
if (!client.userId) {
|
||||||
|
return { error: 'Unauthorized' };
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const isAdmin = client.userRole === 'ADMIN';
|
||||||
|
await this.supportChatService.getChat(data.chatId, client.userId, isAdmin);
|
||||||
|
client.join(`support:${data.chatId}`);
|
||||||
|
this.logger.log(`User ${client.userId} joined support chat ${data.chatId}`);
|
||||||
|
return { success: true };
|
||||||
|
} catch (error) {
|
||||||
|
return { error: error.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Leave a support chat room
|
||||||
|
*/
|
||||||
|
@SubscribeMessage('support_leave')
|
||||||
|
handleSupportLeave(
|
||||||
|
@ConnectedSocket() client: AuthenticatedSocket,
|
||||||
|
@MessageBody() data: { chatId: string },
|
||||||
|
) {
|
||||||
|
client.leave(`support:${data.chatId}`);
|
||||||
|
this.logger.log(`User ${client.userId} left support chat ${data.chatId}`);
|
||||||
|
return { success: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a message in a support chat via WebSocket
|
||||||
|
*/
|
||||||
|
@SubscribeMessage('support_send_message')
|
||||||
|
async handleSupportSendMessage(
|
||||||
|
@ConnectedSocket() client: AuthenticatedSocket,
|
||||||
|
@MessageBody() data: { chatId: string; content: string },
|
||||||
|
) {
|
||||||
|
if (!client.userId) {
|
||||||
|
return { error: 'Unauthorized' };
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const senderRole = client.userRole === 'ADMIN' ? 'ADMIN' : 'USER';
|
||||||
|
const message = await this.supportChatService.sendMessage(
|
||||||
|
data.chatId,
|
||||||
|
client.userId,
|
||||||
|
senderRole,
|
||||||
|
data.content,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Broadcast to all clients in the support chat room
|
||||||
|
this.server.to(`support:${data.chatId}`).emit('support_new_message', message);
|
||||||
|
|
||||||
|
return { success: true, message };
|
||||||
|
} catch (error) {
|
||||||
|
return { error: error.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Support chat typing indicators
|
||||||
|
*/
|
||||||
|
@SubscribeMessage('support_typing_start')
|
||||||
|
handleSupportTypingStart(
|
||||||
|
@ConnectedSocket() client: AuthenticatedSocket,
|
||||||
|
@MessageBody() data: { chatId: string },
|
||||||
|
) {
|
||||||
|
if (!client.userId) return;
|
||||||
|
client.to(`support:${data.chatId}`).emit('support_typing_start', {
|
||||||
|
userId: client.userId,
|
||||||
|
chatId: data.chatId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@SubscribeMessage('support_typing_stop')
|
||||||
|
handleSupportTypingStop(
|
||||||
|
@ConnectedSocket() client: AuthenticatedSocket,
|
||||||
|
@MessageBody() data: { chatId: string },
|
||||||
|
) {
|
||||||
|
if (!client.userId) return;
|
||||||
|
client.to(`support:${data.chatId}`).emit('support_typing_stop', {
|
||||||
|
userId: client.userId,
|
||||||
|
chatId: data.chatId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Broadcast user online/offline status
|
* Broadcast user online/offline status
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -5,10 +5,12 @@ import { MessagesController } from './messages.controller';
|
|||||||
import { MessagesService } from './messages.service';
|
import { MessagesService } from './messages.service';
|
||||||
import { MessagesGateway } from './messages.gateway';
|
import { MessagesGateway } from './messages.gateway';
|
||||||
import { PrismaModule } from '../prisma/prisma.module';
|
import { PrismaModule } from '../prisma/prisma.module';
|
||||||
|
import { SupportChatModule } from '../support-chat/support-chat.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
PrismaModule,
|
PrismaModule,
|
||||||
|
SupportChatModule,
|
||||||
ConfigModule,
|
ConfigModule,
|
||||||
JwtModule.registerAsync({
|
JwtModule.registerAsync({
|
||||||
imports: [ConfigModule],
|
imports: [ConfigModule],
|
||||||
|
|||||||
1
src/support-chat/dto/index.ts
Normal file
1
src/support-chat/dto/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { SendSupportMessageDto } from './send-support-message.dto';
|
||||||
10
src/support-chat/dto/send-support-message.dto.ts
Normal file
10
src/support-chat/dto/send-support-message.dto.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { IsString, MaxLength, IsNotEmpty } from 'class-validator';
|
||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
export class SendSupportMessageDto {
|
||||||
|
@ApiProperty({ description: 'Message content', maxLength: 2000 })
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
@MaxLength(2000)
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
3
src/support-chat/index.ts
Normal file
3
src/support-chat/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export { SupportChatModule } from './support-chat.module';
|
||||||
|
export { SupportChatService } from './support-chat.service';
|
||||||
|
export { SupportChatController } from './support-chat.controller';
|
||||||
137
src/support-chat/support-chat.controller.ts
Normal file
137
src/support-chat/support-chat.controller.ts
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
import {
|
||||||
|
Controller,
|
||||||
|
Get,
|
||||||
|
Post,
|
||||||
|
Patch,
|
||||||
|
Body,
|
||||||
|
Param,
|
||||||
|
Query,
|
||||||
|
UseGuards,
|
||||||
|
HttpCode,
|
||||||
|
HttpStatus,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import {
|
||||||
|
ApiTags,
|
||||||
|
ApiOperation,
|
||||||
|
ApiResponse,
|
||||||
|
ApiBearerAuth,
|
||||||
|
ApiQuery,
|
||||||
|
ApiParam,
|
||||||
|
} from '@nestjs/swagger';
|
||||||
|
import { SupportChatService } from './support-chat.service';
|
||||||
|
import { SendSupportMessageDto } from './dto';
|
||||||
|
import { JwtAuthGuard } from '../auth/guards';
|
||||||
|
import { CurrentUser } from '../auth/decorators';
|
||||||
|
import { Roles } from '../auth/decorators/roles.decorator';
|
||||||
|
import { UserRole, SupportChatStatus } from '@prisma/client';
|
||||||
|
|
||||||
|
@ApiTags('Support Chat')
|
||||||
|
@Controller('support-chat')
|
||||||
|
@UseGuards(JwtAuthGuard)
|
||||||
|
@ApiBearerAuth('JWT-auth')
|
||||||
|
export class SupportChatController {
|
||||||
|
constructor(private readonly supportChatService: SupportChatService) {}
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// GET OR CREATE SUPPORT CHAT (User/Agent)
|
||||||
|
// ==========================================
|
||||||
|
@Post()
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
@ApiOperation({ summary: 'Get or create a support chat for the current user' })
|
||||||
|
async getOrCreateChat(@CurrentUser('id') userId: string) {
|
||||||
|
return this.supportChatService.getOrCreateChat(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// GET MY SUPPORT CHAT (User/Agent)
|
||||||
|
// ==========================================
|
||||||
|
@Get('my')
|
||||||
|
@ApiOperation({ summary: 'Get current user\'s open support chat' })
|
||||||
|
async getMyChat(@CurrentUser('id') userId: string) {
|
||||||
|
return this.supportChatService.getOrCreateChat(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// GET MESSAGES
|
||||||
|
// ==========================================
|
||||||
|
@Get(':id/messages')
|
||||||
|
@ApiOperation({ summary: 'Get messages for a support chat' })
|
||||||
|
@ApiParam({ name: 'id', description: 'Support Chat ID' })
|
||||||
|
@ApiQuery({ name: 'page', required: false, type: Number })
|
||||||
|
@ApiQuery({ name: 'limit', required: false, type: Number })
|
||||||
|
async getMessages(
|
||||||
|
@Param('id') chatId: string,
|
||||||
|
@CurrentUser('id') userId: string,
|
||||||
|
@CurrentUser('role') role: UserRole,
|
||||||
|
@Query('page') page?: string,
|
||||||
|
@Query('limit') limit?: string,
|
||||||
|
) {
|
||||||
|
// Validate access
|
||||||
|
const isAdmin = role === UserRole.ADMIN;
|
||||||
|
await this.supportChatService.getChat(chatId, userId, isAdmin);
|
||||||
|
|
||||||
|
return this.supportChatService.getMessages(
|
||||||
|
chatId,
|
||||||
|
page ? parseInt(page, 10) : 1,
|
||||||
|
limit ? parseInt(limit, 10) : 50,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// SEND MESSAGE (REST fallback)
|
||||||
|
// ==========================================
|
||||||
|
@Post(':id/messages')
|
||||||
|
@HttpCode(HttpStatus.CREATED)
|
||||||
|
@ApiOperation({ summary: 'Send a message in a support chat' })
|
||||||
|
@ApiParam({ name: 'id', description: 'Support Chat ID' })
|
||||||
|
async sendMessage(
|
||||||
|
@Param('id') chatId: string,
|
||||||
|
@CurrentUser('id') userId: string,
|
||||||
|
@CurrentUser('role') role: UserRole,
|
||||||
|
@Body() dto: SendSupportMessageDto,
|
||||||
|
) {
|
||||||
|
// Validate access
|
||||||
|
const isAdmin = role === UserRole.ADMIN;
|
||||||
|
await this.supportChatService.getChat(chatId, userId, isAdmin);
|
||||||
|
|
||||||
|
const senderRole = role === UserRole.ADMIN ? 'ADMIN' : 'USER';
|
||||||
|
return this.supportChatService.sendMessage(chatId, userId, senderRole, dto.content);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// MARK AS READ
|
||||||
|
// ==========================================
|
||||||
|
@Patch(':id/read')
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
@ApiOperation({ summary: 'Mark support chat messages as read' })
|
||||||
|
@ApiParam({ name: 'id', description: 'Support Chat ID' })
|
||||||
|
async markAsRead(
|
||||||
|
@Param('id') chatId: string,
|
||||||
|
@CurrentUser('role') role: UserRole,
|
||||||
|
) {
|
||||||
|
return this.supportChatService.markAsRead(chatId, role);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// ADMIN: GET ALL SUPPORT CHATS
|
||||||
|
// ==========================================
|
||||||
|
@Get('admin/all')
|
||||||
|
@Roles(UserRole.ADMIN)
|
||||||
|
@ApiOperation({ summary: 'Admin: Get all support chats' })
|
||||||
|
@ApiQuery({ name: 'status', required: false, enum: SupportChatStatus })
|
||||||
|
async getAllChats(@Query('status') status?: SupportChatStatus) {
|
||||||
|
return this.supportChatService.getAllChats(status);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// ADMIN: CLOSE SUPPORT CHAT
|
||||||
|
// ==========================================
|
||||||
|
@Patch('admin/:id/close')
|
||||||
|
@Roles(UserRole.ADMIN)
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
@ApiOperation({ summary: 'Admin: Close a support chat' })
|
||||||
|
@ApiParam({ name: 'id', description: 'Support Chat ID' })
|
||||||
|
async closeChat(@Param('id') chatId: string) {
|
||||||
|
return this.supportChatService.closeChat(chatId);
|
||||||
|
}
|
||||||
|
}
|
||||||
12
src/support-chat/support-chat.module.ts
Normal file
12
src/support-chat/support-chat.module.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { SupportChatController } from './support-chat.controller';
|
||||||
|
import { SupportChatService } from './support-chat.service';
|
||||||
|
import { PrismaModule } from '../prisma/prisma.module';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [PrismaModule],
|
||||||
|
controllers: [SupportChatController],
|
||||||
|
providers: [SupportChatService],
|
||||||
|
exports: [SupportChatService],
|
||||||
|
})
|
||||||
|
export class SupportChatModule {}
|
||||||
219
src/support-chat/support-chat.service.ts
Normal file
219
src/support-chat/support-chat.service.ts
Normal file
@@ -0,0 +1,219 @@
|
|||||||
|
import {
|
||||||
|
Injectable,
|
||||||
|
NotFoundException,
|
||||||
|
ForbiddenException,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { PrismaService } from '../prisma/prisma.service';
|
||||||
|
import { SupportChatStatus } from '@prisma/client';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class SupportChatService {
|
||||||
|
constructor(private readonly prisma: PrismaService) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get or create an OPEN support chat for a user
|
||||||
|
*/
|
||||||
|
async getOrCreateChat(userId: string) {
|
||||||
|
// Find existing OPEN chat
|
||||||
|
let chat = await this.prisma.supportChat.findFirst({
|
||||||
|
where: { userId, status: SupportChatStatus.OPEN },
|
||||||
|
include: {
|
||||||
|
user: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
email: true,
|
||||||
|
userProfile: {
|
||||||
|
select: { firstName: true, lastName: true, avatar: true },
|
||||||
|
},
|
||||||
|
agentProfile: {
|
||||||
|
select: { firstName: true, lastName: true, avatar: true },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!chat) {
|
||||||
|
chat = await this.prisma.supportChat.create({
|
||||||
|
data: { userId },
|
||||||
|
include: {
|
||||||
|
user: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
email: true,
|
||||||
|
userProfile: {
|
||||||
|
select: { firstName: true, lastName: true, avatar: true },
|
||||||
|
},
|
||||||
|
agentProfile: {
|
||||||
|
select: { firstName: true, lastName: true, avatar: true },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return chat;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a chat by ID with access validation
|
||||||
|
*/
|
||||||
|
async getChat(chatId: string, userId?: string, isAdmin = false) {
|
||||||
|
const chat = await this.prisma.supportChat.findUnique({
|
||||||
|
where: { id: chatId },
|
||||||
|
include: {
|
||||||
|
user: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
email: true,
|
||||||
|
userProfile: {
|
||||||
|
select: { firstName: true, lastName: true, avatar: true },
|
||||||
|
},
|
||||||
|
agentProfile: {
|
||||||
|
select: { firstName: true, lastName: true, avatar: true },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!chat) {
|
||||||
|
throw new NotFoundException('Support chat not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isAdmin && userId && chat.userId !== userId) {
|
||||||
|
throw new ForbiddenException('You do not have access to this chat');
|
||||||
|
}
|
||||||
|
|
||||||
|
return chat;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get messages for a support chat with pagination
|
||||||
|
*/
|
||||||
|
async getMessages(chatId: string, page = 1, limit = 50) {
|
||||||
|
const skip = (page - 1) * limit;
|
||||||
|
|
||||||
|
const [messages, total] = await Promise.all([
|
||||||
|
this.prisma.supportMessage.findMany({
|
||||||
|
where: { chatId },
|
||||||
|
orderBy: { createdAt: 'desc' },
|
||||||
|
skip,
|
||||||
|
take: limit,
|
||||||
|
}),
|
||||||
|
this.prisma.supportMessage.count({ where: { chatId } }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
messages: messages.reverse(),
|
||||||
|
pagination: {
|
||||||
|
page,
|
||||||
|
limit,
|
||||||
|
total,
|
||||||
|
pages: Math.ceil(total / limit),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a message in a support chat
|
||||||
|
*/
|
||||||
|
async sendMessage(chatId: string, senderId: string, senderRole: string, content: string) {
|
||||||
|
const chat = await this.prisma.supportChat.findUnique({
|
||||||
|
where: { id: chatId },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!chat) {
|
||||||
|
throw new NotFoundException('Support chat not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (chat.status === SupportChatStatus.CLOSED) {
|
||||||
|
throw new ForbiddenException('This support chat is closed');
|
||||||
|
}
|
||||||
|
|
||||||
|
const message = await this.prisma.supportMessage.create({
|
||||||
|
data: {
|
||||||
|
chatId,
|
||||||
|
senderId,
|
||||||
|
senderRole,
|
||||||
|
content,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update chat metadata
|
||||||
|
const truncatedContent =
|
||||||
|
content.length > 255 ? content.substring(0, 252) + '...' : content;
|
||||||
|
|
||||||
|
const isUser = senderRole === 'USER' || senderRole === 'AGENT';
|
||||||
|
await this.prisma.supportChat.update({
|
||||||
|
where: { id: chatId },
|
||||||
|
data: {
|
||||||
|
lastMessageAt: new Date(),
|
||||||
|
lastMessageText: truncatedContent,
|
||||||
|
...(isUser
|
||||||
|
? { adminUnreadCount: { increment: 1 } }
|
||||||
|
: { userUnreadCount: { increment: 1 } }),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark messages as read for a role
|
||||||
|
*/
|
||||||
|
async markAsRead(chatId: string, role: string) {
|
||||||
|
const isUser = role === 'USER' || role === 'AGENT';
|
||||||
|
await this.prisma.supportChat.update({
|
||||||
|
where: { id: chatId },
|
||||||
|
data: isUser ? { userUnreadCount: 0 } : { adminUnreadCount: 0 },
|
||||||
|
});
|
||||||
|
return { success: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Admin: get all support chats
|
||||||
|
*/
|
||||||
|
async getAllChats(status?: SupportChatStatus) {
|
||||||
|
const where = status ? { status } : {};
|
||||||
|
|
||||||
|
return this.prisma.supportChat.findMany({
|
||||||
|
where,
|
||||||
|
include: {
|
||||||
|
user: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
email: true,
|
||||||
|
role: true,
|
||||||
|
userProfile: {
|
||||||
|
select: { firstName: true, lastName: true, avatar: true },
|
||||||
|
},
|
||||||
|
agentProfile: {
|
||||||
|
select: { firstName: true, lastName: true, avatar: true },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
orderBy: { lastMessageAt: 'desc' },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Admin: close a support chat
|
||||||
|
*/
|
||||||
|
async closeChat(chatId: string) {
|
||||||
|
const chat = await this.prisma.supportChat.findUnique({
|
||||||
|
where: { id: chatId },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!chat) {
|
||||||
|
throw new NotFoundException('Support chat not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.prisma.supportChat.update({
|
||||||
|
where: { id: chatId },
|
||||||
|
data: { status: SupportChatStatus.CLOSED },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user