- Comment module with CRUD and file upload via multer - @mention parsing with user notification - Assignment notification on ticket update - Notification service enhanced with ticketId linking - ServeStaticModule for uploaded files
118 lines
3.7 KiB
TypeScript
118 lines
3.7 KiB
TypeScript
import { Injectable, NotFoundException } from '@nestjs/common';
|
|
import { PrismaService } from '../prisma/prisma.service';
|
|
import { NotificationService } from '../notification/notification.service';
|
|
|
|
@Injectable()
|
|
export class CommentService {
|
|
constructor(
|
|
private readonly prisma: PrismaService,
|
|
private readonly notificationService: NotificationService,
|
|
) {}
|
|
|
|
async findAll(tenantId: string, ticketId: string) {
|
|
const db = this.prisma.forTenant(tenantId);
|
|
const ticket = await db.ticket.findFirst({ where: { id: ticketId } });
|
|
if (!ticket) throw new NotFoundException('Ticket not found');
|
|
|
|
return this.prisma.ticketComment.findMany({
|
|
where: { tenantId, ticketId },
|
|
include: {
|
|
author: { select: { id: true, firstName: true, lastName: true } },
|
|
attachments: true,
|
|
},
|
|
orderBy: { createdAt: 'asc' },
|
|
});
|
|
}
|
|
|
|
async create(
|
|
tenantId: string,
|
|
ticketId: string,
|
|
userId: string,
|
|
content: string,
|
|
files?: Express.Multer.File[],
|
|
) {
|
|
const db = this.prisma.forTenant(tenantId);
|
|
const ticket = await db.ticket.findFirst({ where: { id: ticketId } });
|
|
if (!ticket) throw new NotFoundException('Ticket not found');
|
|
|
|
const comment = await this.prisma.ticketComment.create({
|
|
data: {
|
|
tenantId,
|
|
ticketId,
|
|
userId,
|
|
content,
|
|
attachments: files?.length
|
|
? {
|
|
create: files.map((f) => ({
|
|
fileName: f.originalname,
|
|
filePath: f.filename,
|
|
fileType: f.mimetype,
|
|
fileSize: f.size,
|
|
})),
|
|
}
|
|
: undefined,
|
|
},
|
|
include: {
|
|
author: { select: { id: true, firstName: true, lastName: true } },
|
|
attachments: true,
|
|
},
|
|
});
|
|
|
|
// Parse @mentions and notify mentioned users
|
|
const mentions = this.parseMentions(content);
|
|
if (mentions.length > 0) {
|
|
const db2 = this.prisma.forTenant(tenantId);
|
|
const users = await db2.user.findMany({
|
|
where: { tenantId, isActive: true },
|
|
select: { id: true, firstName: true, lastName: true },
|
|
});
|
|
|
|
const commenterName = `${comment.author.firstName} ${comment.author.lastName}`;
|
|
|
|
for (const mention of mentions) {
|
|
const mentionedUser = users.find(
|
|
(u) =>
|
|
`${u.firstName} ${u.lastName}`.toLowerCase() === mention.toLowerCase() ||
|
|
u.firstName.toLowerCase() === mention.toLowerCase(),
|
|
);
|
|
if (mentionedUser && mentionedUser.id !== userId) {
|
|
await this.notificationService.create(tenantId, {
|
|
userId: mentionedUser.id,
|
|
type: 'in_app',
|
|
channel: 'mention',
|
|
title: 'You were mentioned',
|
|
message: `${commenterName} mentioned you in "${ticket.title}"`,
|
|
ticketId,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
// Notify ticket creator (if not the commenter)
|
|
if (ticket.createdById && ticket.createdById !== userId) {
|
|
const commenterName = `${comment.author.firstName} ${comment.author.lastName}`;
|
|
await this.notificationService.create(tenantId, {
|
|
userId: ticket.createdById,
|
|
type: 'in_app',
|
|
channel: 'comment_added',
|
|
title: 'New comment on your ticket',
|
|
message: `${commenterName} commented on "${ticket.title}"`,
|
|
ticketId,
|
|
});
|
|
}
|
|
|
|
return comment;
|
|
}
|
|
|
|
/** Extract @FirstName or @FirstNameLastName from content */
|
|
private parseMentions(content: string): string[] {
|
|
const regex = /@(\w+(?:\s+\w+)?)/g;
|
|
const matches: string[] = [];
|
|
let match: RegExpExecArray | null;
|
|
while ((match = regex.exec(content)) !== null) {
|
|
matches.push(match[1]);
|
|
}
|
|
return [...new Set(matches)];
|
|
}
|
|
}
|