import { BadRequestException, HttpException, Injectable, InternalServerErrorException, Logger } from "@nestjs/common"; import { DataSource, Not } from "typeorm"; import { ParamDto } from "../../../common/DTO/param.dto"; import { CommonMessage, TicketMessageEnum, UserMessage } from "../../../common/enums/message.enum"; import { DanakService } from "../../danak-services/entities/danak-service.entity"; import { NotificationsService } from "../../notifications/providers/notifications.service"; import { User } from "../../users/entities/user.entity"; import { UsersService } from "../../users/providers/users.service"; import { PaginationUtils } from "../../utils/providers/pagination.utils"; import { CreateTicketCategoryDto } from "../DTO/create-ticket-category.dto"; import { CreateTicketMessageDto } from "../DTO/create-ticket-message.dto"; import { CreateTicketDto } from "../DTO/create-ticket.dto"; import { SearchTicketCategoryDto } from "../DTO/search-ticket-category.dto"; import { SearchTicketQueryDto } from "../DTO/search-ticket-query.dto"; import { UpdateTicketCategoryDto } from "../DTO/update-ticket-category.dto"; import { TicketCategory } from "../entities/ticket-category.entity"; import { TicketMessageAttachment } from "../entities/ticket-message-attachment"; import { TicketMessage } from "../entities/ticket-message.entity"; import { Ticket } from "../entities/ticket.entity"; import { TicketStatus } from "../enums/ticket-status.enum"; import { TicketCategoryRepository } from "../repositories/tickets-category.repository"; import { TicketMessagesRepository } from "../repositories/tickets-message.repository"; import { TicketsRepository } from "../repositories/tickets.repository"; @Injectable() export class TicketsService { private readonly logger = new Logger(TicketsService.name); constructor( private readonly notificationsService: NotificationsService, private readonly ticketsCategoryRepository: TicketCategoryRepository, private readonly ticketsRepository: TicketsRepository, private readonly ticketMessagesRepository: TicketMessagesRepository, private readonly usersService: UsersService, private dataSource: DataSource, ) {} //******************************** */ async getTicketCategories() { const ticketCategories = await this.ticketsCategoryRepository.find({}); return { ticketCategories, }; } //******************************** */ async getCategoriesList(queryDto: SearchTicketCategoryDto) { const { limit, skip } = PaginationUtils(queryDto); const queryBuilder = this.ticketsCategoryRepository .createQueryBuilder("category") // .leftJoinAndSelect("category.group", "group") // .addSelect(["group.id", "group.name"]) .leftJoin("category.group", "group") .addSelect(["group.id", "group.name"]) // .leftJoinAndSelect("group.users", "users") .loadRelationCountAndMap("category.ticketsCount", "category.tickets") .loadRelationCountAndMap("category.usersCount", "group.users"); if (queryDto.isActive !== undefined) { queryBuilder.andWhere("category.isActive = :isActive", { isActive: queryDto.isActive === 1 }); } if (queryDto.q) { queryBuilder.where("category.title LIKE :q", { q: `%${queryDto.q}%` }); } const [categories, count] = await queryBuilder.skip(skip).take(limit).getManyAndCount(); return { categories, count, paginate: true }; } //******************************** */ async createTicketCategory(createDto: CreateTicketCategoryDto) { const { userGroup } = await this.usersService.getUserGroupById(createDto.userGroupId); const existCategory = await this.ticketsCategoryRepository.findCategoryByTitle(createDto.title); if (existCategory) throw new BadRequestException(TicketMessageEnum.CATEGORY_EXIST); const ticketCategory = this.ticketsCategoryRepository.create({ ...createDto, group: userGroup }); await this.ticketsCategoryRepository.save(ticketCategory); return { message: TicketMessageEnum.CATEGORY_CREATED, }; } //******************************** */ async updateCategory(paramDto: ParamDto, updateCategoryDto: UpdateTicketCategoryDto) { const category = await this.ticketsCategoryRepository.findCategoryById(paramDto.id); if (!category) throw new BadRequestException(TicketMessageEnum.CATEGORY_NOT_FOUND); if (updateCategoryDto.title) { const { title } = updateCategoryDto; const existCategory = await this.ticketsCategoryRepository.findOneBy({ title, id: Not(paramDto.id) }); if (existCategory) throw new BadRequestException(TicketMessageEnum.CATEGORY_EXIST); } await this.ticketsCategoryRepository.save({ ...category, ...updateCategoryDto }); return { message: CommonMessage.UPDATE_SUCCESS, }; } //******************************** */ async toggleCategoryStatus(paramDto: ParamDto) { const category = await this.ticketsCategoryRepository.findCategoryById(paramDto.id); if (!category) throw new BadRequestException(TicketMessageEnum.CATEGORY_NOT_FOUND); // category.isActive = !category.isActive; await this.ticketsCategoryRepository.save(category); // return { message: CommonMessage.UPDATE_SUCCESS, isActive: category.isActive, }; } //******************************** */ async createTicket(createDto: CreateTicketDto, userId: string) { const queryRunner = this.dataSource.createQueryRunner(); await queryRunner.connect(); await queryRunner.startTransaction(); try { let service; const user = await this.usersService.findOneByIdWithQueryRunner(userId, queryRunner); const ticketCategory = await queryRunner.manager.findOne(TicketCategory, { where: { id: createDto.categoryId }, relations: { group: { users: true } }, }); if (!ticketCategory) throw new BadRequestException(TicketMessageEnum.CATEGORY_NOT_FOUND); const { attachmentUrls } = createDto; // if (createDto.danakServiceId) { service = await queryRunner.manager.findOneBy(DanakService, { id: createDto.danakServiceId }); if (!service) throw new BadRequestException(TicketMessageEnum.SERVICE_NOT_FOUND); } const ticketMessage: Partial = { content: createDto.message, author: user }; if (attachmentUrls?.length) { ticketMessage.attachments = attachmentUrls.map((url) => { const attachment = new TicketMessageAttachment(); attachment.attachmentUrl = url; return attachment; }); } const ticket = queryRunner.manager.create(Ticket, { ...createDto, user, category: ticketCategory, messages: [ticketMessage], danakService: service, }); await queryRunner.manager.save(Ticket, ticket); const assignedUser = await this.autoAssignUser(ticketCategory); //send notification to assignedUser if (assignedUser) { ticket.assignedTo = assignedUser; await queryRunner.manager.save(Ticket, ticket); await this.notificationsService.createAssignTicketNotificationForAdmin( assignedUser.id, { ticketId: ticket.numericId.toString(), subject: ticket.subject, date: ticket.createdAt, userPhone: user.phone, userEmail: user.email, user: `${user.firstName} ${user.lastName}`, }, queryRunner, ); } await this.notificationsService.createTicketNotification( userId, { ticketId: ticket.numericId.toString(), subject: ticket.subject, date: ticket.createdAt, userPhone: user.phone, userEmail: user.email, }, queryRunner, ); await queryRunner.commitTransaction(); return { message: TicketMessageEnum.CREATED, ticketId: ticket.id, }; } catch (error) { this.logger.error("Error in create ticket transaction", error); await queryRunner.rollbackTransaction(); throw error; } finally { await queryRunner.release(); } } //******************************** */ async getTickets(queryDto: SearchTicketQueryDto, userId: string) { const { limit, skip } = PaginationUtils(queryDto); const findOptions = { where: { user: { id: userId }, ...(queryDto.status ? { status: queryDto.status } : { status: Not(TicketStatus.CLOSED) }), }, relations: { category: true, danakService: true, }, select: { id: true, subject: true, status: true, priority: true, numericId: true, createdAt: true, updatedAt: true, category: { id: true, title: true, }, danakService: { id: true, name: true, }, }, order: { createdAt: "DESC" as const, }, skip, take: limit, }; const [tickets, count] = await this.ticketsRepository.findAndCount(findOptions); return { tickets, count, paginate: true }; } //******************************** */ async getTicketForAdmin(queryDto: SearchTicketQueryDto, adminId: string) { const { limit, skip } = PaginationUtils(queryDto); const isSuperAdmin = await this.usersService.isSuperAdmin(adminId); const queryBuilder = this.ticketsRepository .createQueryBuilder("ticket") .leftJoinAndSelect("ticket.category", "category") .leftJoinAndSelect("ticket.user", "user") .leftJoinAndSelect("ticket.assignedTo", "assignedTo") .leftJoinAndSelect("ticket.danakService", "danakService") .orderBy("ticket.createdAt", "DESC"); if (!isSuperAdmin) { queryBuilder.andWhere("ticket.assignedToId = :adminId", { adminId }); } if (queryDto.status) { queryBuilder.andWhere("ticket.status = :status", { status: queryDto.status }); } if (queryDto.userId) { queryBuilder.andWhere("user.id = :userId", { userId: queryDto.userId }); } const [tickets, count] = await queryBuilder.skip(skip).take(limit).getManyAndCount(); return { tickets, count, paginate: true }; } //******************************** */ async createTicketMessage(ticketId: string, createDto: CreateTicketMessageDto, userId: string, isAdmin: boolean) { const queryRunner = this.dataSource.createQueryRunner(); await queryRunner.connect(); await queryRunner.startTransaction(); try { let ticket: null | Ticket = null; if (isAdmin) { ticket = await queryRunner.manager.findOne(Ticket, { where: { id: ticketId }, relations: { user: true } }); } else { ticket = await queryRunner.manager.findOne(Ticket, { where: { id: ticketId, user: { id: userId } }, relations: { user: true } }); } if (!ticket) throw new BadRequestException(TicketMessageEnum.TICKET_NOT_FOUND); if (ticket.status === TicketStatus.CLOSED) throw new BadRequestException(TicketMessageEnum.TICKET_CLOSED); const user = await queryRunner.manager.findOneBy(User, { id: userId }); if (!user) throw new BadRequestException(UserMessage.USER_NOT_FOUND); const ticketMessage = queryRunner.manager.create(TicketMessage, { ...createDto, author: user, ticket }); if (isAdmin && ticket.status === TicketStatus.PENDING) { ticket.status = TicketStatus.ANSWERED; } else if (!isAdmin && ticket.status === TicketStatus.ANSWERED) { ticket.status = TicketStatus.PENDING; } await queryRunner.manager.save(Ticket, ticket); await queryRunner.manager.save(TicketMessage, ticketMessage); if (createDto.attachmentUrls) { const attachments = createDto.attachmentUrls.map((url) => queryRunner.manager.create(TicketMessageAttachment, { attachmentUrl: url, ticketMessage }), ); await queryRunner.manager.save(TicketMessageAttachment, attachments); } if (isAdmin) { await this.notificationsService.createAnswerTicketNotification( ticket.user.id, { ticketId: ticket.numericId.toString(), subject: ticket.subject, date: ticket.createdAt, userPhone: user.phone, userEmail: user.email, }, queryRunner, ); } await queryRunner.commitTransaction(); return { message: TicketMessageEnum.MESSAGE_CREATED, ticketMessage, }; } catch (error) { await queryRunner.rollbackTransaction(); if (error instanceof HttpException) throw error; throw new InternalServerErrorException(TicketMessageEnum.ERROR_IN_TICKET_MSG_CREATION); } finally { await queryRunner.release(); } } //******************************** */ async getTicketMessages(ticketId: string, userId: string, isAdmin: boolean) { let ticket: null | Ticket = null; // if (isAdmin) { ticket = await this.ticketsRepository.findTicketById(ticketId); } else { ticket = await this.ticketsRepository.findTicketById(ticketId, userId); } if (!ticket) throw new BadRequestException(TicketMessageEnum.TICKET_NOT_FOUND); const messages = await this.ticketMessagesRepository.findMessagesByTicketId(ticketId); return { ticket, messages, }; } //******************************** */ async closeTicketByUser(ticketId: string, userId: string, isAdmin: boolean) { let ticket: null | Ticket = null; if (isAdmin) { ticket = await this.ticketsRepository.findTicketById(ticketId); } else { ticket = await this.ticketsRepository.findTicketById(ticketId, userId); } if (!ticket) throw new BadRequestException(TicketMessageEnum.TICKET_NOT_FOUND); ticket.status = TicketStatus.CLOSED; await this.ticketsRepository.save(ticket); return { message: TicketMessageEnum.CLOSED, }; } //******************************** */ async getUnreadTickets() { const unreadTickets = await this.ticketsRepository.count({ where: { status: TicketStatus.PENDING, }, }); return unreadTickets; } //******************************** */ async countUserTickets(userId: string) { const ticketCount = await this.ticketsRepository.count({ where: { user: { id: userId }, status: Not(TicketStatus.CLOSED), }, }); return ticketCount; } //******************************** */ async autoAssignUser(category: TicketCategory): Promise { if (!category.group) return null; const { users } = await this.usersService.getUsersByGroup(category.group.id); if (users.length === 0) return null; users.sort((a, b) => (a.tickets.length || 0) - (b.tickets.length || 0)); return users[0]; } }