fix: bug in create company
This commit is contained in:
@@ -18,6 +18,7 @@ import { AuthModule } from "./modules/auth/auth.module";
|
||||
import { CompaniesModule } from "./modules/companies/companies.module";
|
||||
import { IndustriesModule } from "./modules/industries/industries.module";
|
||||
import { NotificationModule } from "./modules/notifications/notifications.module";
|
||||
import { TicketsModule } from "./modules/tickets/tickets.module";
|
||||
import { UploaderModule } from "./modules/uploader/uploader.module";
|
||||
import { UsersModule } from "./modules/users/users.module";
|
||||
import { UtilsModule } from "./modules/utils/utils.module";
|
||||
@@ -46,6 +47,7 @@ import { UtilsModule } from "./modules/utils/utils.module";
|
||||
IndustriesModule,
|
||||
CompaniesModule,
|
||||
UploaderModule,
|
||||
TicketsModule,
|
||||
],
|
||||
})
|
||||
export class AppModule implements NestModule {
|
||||
|
||||
@@ -259,8 +259,8 @@ export const enum TicketMessageEnum {
|
||||
PRIORITY_INVALID = "اولویت تیکت باید یکی از مقادیر LOW، MEDIUM یا HIGH باشد",
|
||||
CATEGORY_ID_REQUIRED = "شناسه دستهبندی نمیتواند خالی باشد",
|
||||
CATEGORY_ID_SHOULD_BE_UUID = "شناسه دستهبندی باید یک UUID معتبر باشد",
|
||||
DANAK_SERVICE_ID_REQUIRED = "شناسه سرویس داناک اجباری است",
|
||||
DANAK_SERVICE_ID_SHOULD_BE_UUID = "شناسه سرویس داناک باید یک UUID معتبر باشد",
|
||||
COMPANY_ID_REQUIRED = "شناسه شرکت اجباری است",
|
||||
COMPANY_ID_SHOULD_BE_UUID = "شناسه شرکت باید یک UUID معتبر باشد",
|
||||
MESSAGE_REQUIRED = "متن پیام تیکت اجباری است",
|
||||
MESSAGE_STRING = "متن پیام باید یک رشته باشد",
|
||||
MESSAGE_LENGTH = "متن پیام باید بین ۵ تا ۵۰۰ کاراکتر باشد",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Collection, EntityManager } from "@mikro-orm/postgresql";
|
||||
import { EntityManager } from "@mikro-orm/postgresql";
|
||||
import { BadRequestException, Injectable } from "@nestjs/common";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
@@ -28,7 +28,7 @@ export class CompaniesService {
|
||||
async create(createDto: CreateCompanyDto) {
|
||||
const em = this.em.fork();
|
||||
try {
|
||||
em.begin();
|
||||
await em.begin();
|
||||
|
||||
const industry = await em.findOne(Industry, { id: createDto.industryId, deletedAt: null });
|
||||
if (!industry) throw new BadRequestException(IndustryMessage.INDUSTRY_NOT_FOUND);
|
||||
@@ -44,33 +44,48 @@ export class CompaniesService {
|
||||
const hashedPassword = await this.passwordService.hashPassword(createDto.password);
|
||||
const user = await this.usersService.createUser(createDto, hashedPassword, em);
|
||||
|
||||
const company = em.create(Company, { ...createDto, industry, status: CompanyStatus.APPROVED, user });
|
||||
const products = createDto.products.map((productData) => {
|
||||
const product = new CompanyProduct();
|
||||
product.title = productData.title;
|
||||
product.imagesUrl = productData.imagesUrl;
|
||||
return product;
|
||||
});
|
||||
|
||||
const products = createDto.products.map((productData) => em.create(CompanyProduct, { ...productData, company }));
|
||||
company.products = new Collection(products);
|
||||
const services = createDto.services.map((serviceData) => {
|
||||
const service = new CompanyService();
|
||||
service.title = serviceData.title;
|
||||
service.imagesUrl = serviceData.imagesUrl;
|
||||
return service;
|
||||
});
|
||||
|
||||
const services = createDto.services.map((serviceData) => em.create(CompanyService, { ...serviceData, company }));
|
||||
company.services = new Collection(services);
|
||||
const company = em.create(Company, {
|
||||
...createDto,
|
||||
industry,
|
||||
status: CompanyStatus.APPROVED,
|
||||
products,
|
||||
services,
|
||||
user,
|
||||
});
|
||||
|
||||
await em.persistAndFlush(company);
|
||||
await em.commit();
|
||||
|
||||
return {
|
||||
message: CompanyMessage.COMPANY_CREATED_SUCCESSFULLY,
|
||||
company,
|
||||
user,
|
||||
company: company.name,
|
||||
};
|
||||
} catch (error) {
|
||||
await em.rollback();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
//-----------------------------------
|
||||
|
||||
async updateCompanyById(id: string, updateDto: UpdateCompanyDto) {
|
||||
const em = this.em.fork();
|
||||
try {
|
||||
em.begin();
|
||||
await em.begin();
|
||||
|
||||
const company = await em.findOne(Company, { id, deletedAt: null }, { populate: ["user"] });
|
||||
if (!company) throw new BadRequestException(CompanyMessage.COMPANY_NOT_FOUND);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Body, Controller, Delete, Get, Param, Patch, Post, Query } from "@nestjs/common";
|
||||
import { ApiOperation } from "@nestjs/swagger";
|
||||
|
||||
import { CreateIndustryDto } from "./DTO/create-industry.dto";
|
||||
import { IndustryListQueryDto } from "./DTO/industry-list-query.dto";
|
||||
@@ -11,31 +12,37 @@ export class IndustriesController {
|
||||
constructor(private readonly industriesService: IndustriesService) {}
|
||||
|
||||
@Post()
|
||||
@ApiOperation({ summary: "create industry" })
|
||||
create(@Body() createIndustryDto: CreateIndustryDto) {
|
||||
return this.industriesService.create(createIndustryDto);
|
||||
}
|
||||
|
||||
@Get(":id")
|
||||
@ApiOperation({ summary: "get industry by id" })
|
||||
getIndustryById(@Param() paramDto: ParamDto) {
|
||||
return this.industriesService.getIndustriesById(paramDto.id);
|
||||
}
|
||||
|
||||
@Patch(":id")
|
||||
@ApiOperation({ summary: "update industry" })
|
||||
updateIndustry(@Param() paramDto: ParamDto, @Body() updateIndustryDto: UpdateIndustryDto) {
|
||||
return this.industriesService.updateIndustry(paramDto.id, updateIndustryDto);
|
||||
}
|
||||
|
||||
@Delete(":id")
|
||||
@ApiOperation({ summary: "delete industry" })
|
||||
deleteIndustry(@Param() paramDto: ParamDto) {
|
||||
return this.industriesService.deleteIndustry(paramDto.id);
|
||||
}
|
||||
|
||||
@Get("list")
|
||||
@ApiOperation({ summary: "get industries list for admin" })
|
||||
getIndustriesForAdmin(@Query() query: IndustryListQueryDto) {
|
||||
return this.industriesService.getIndustriesListForAdmin(query);
|
||||
}
|
||||
|
||||
@Patch(":id/toggle-status")
|
||||
@ApiOperation({ summary: "toggle industry status" })
|
||||
toggleStatus(@Param() paramDto: ParamDto) {
|
||||
return this.industriesService.toggleStatus(paramDto.id);
|
||||
}
|
||||
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { IsBoolean, IsNotEmpty, IsString, IsUUID, Length } from "class-validator";
|
||||
|
||||
import { TicketMessageEnum } from "../../../common/enums/message.enum";
|
||||
|
||||
export class CreateTicketCategoryDto {
|
||||
@IsNotEmpty({ message: TicketMessageEnum.TITLE_REQUIRED })
|
||||
@IsString({ message: TicketMessageEnum.TITLE_STRING })
|
||||
@Length(3, 150, { message: TicketMessageEnum.TITLE_LENGTH })
|
||||
@ApiProperty({ description: "Title of the ticket category", example: "فنی", minLength: 3, maxLength: 150 })
|
||||
title: string;
|
||||
|
||||
@IsNotEmpty({ message: TicketMessageEnum.DESCRIPTION_REQUIRED })
|
||||
@IsString({ message: TicketMessageEnum.DESCRIPTION_STRING })
|
||||
@Length(3, 500, { message: TicketMessageEnum.DESCRIPTION_LENGTH })
|
||||
@ApiProperty({ description: "Description of the ticket category", example: "درخواستهای فنی", minLength: 3, maxLength: 500 })
|
||||
description: string;
|
||||
|
||||
@IsNotEmpty({ message: TicketMessageEnum.USER_GROUP_ID_NOT_EMPTY })
|
||||
@IsUUID("4", { message: TicketMessageEnum.USER_GROUP_ID_SHOULD_BE_UUID })
|
||||
@ApiProperty({ description: "User group ID", example: "123e4567-e89b-12d3-a456-426614174000" })
|
||||
userGroupId: string;
|
||||
|
||||
@IsNotEmpty({ message: TicketMessageEnum.IS_ACTIVE_REQUIRED })
|
||||
@IsBoolean({ message: TicketMessageEnum.IS_ACTIVE_SHOULD_BE_BOOLEAN })
|
||||
@ApiProperty({ description: "Is active", example: true })
|
||||
isActive: boolean;
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { ArrayMinSize, IsNotEmpty, IsOptional, IsString, IsUrl } from "class-validator";
|
||||
|
||||
import { TicketMessageEnum } from "../../../common/enums/message.enum";
|
||||
|
||||
export class CreateTicketMessageDto {
|
||||
@IsNotEmpty({ message: TicketMessageEnum.CONTENT_REQUIRED })
|
||||
@IsString({ message: TicketMessageEnum.CONTENT_STRING })
|
||||
@ApiProperty({ description: "the content of message", example: "test message" })
|
||||
content: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsNotEmpty({ message: TicketMessageEnum.ATTACHMENT_REQUIRED })
|
||||
@ArrayMinSize(1, { message: TicketMessageEnum.ATTACHMENT_REQUIRED })
|
||||
@IsUrl({ protocols: ["http", "https"], require_protocol: true }, { each: true, message: TicketMessageEnum.ATTACHMENT_SHOULD_BE_URL })
|
||||
@ApiProperty({ description: "attachment url", example: ["https://example.com/test.png"] })
|
||||
attachmentUrls?: string[];
|
||||
}
|
||||
Executable
+42
@@ -0,0 +1,42 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { ArrayMinSize, IsEnum, IsNotEmpty, IsOptional, IsString, IsUUID, IsUrl, Length } from "class-validator";
|
||||
|
||||
import { TicketMessageEnum } from "../../../common/enums/message.enum";
|
||||
import { TicketPriority } from "../enums/ticket-priority.enum";
|
||||
|
||||
export class CreateTicketDto {
|
||||
@IsNotEmpty({ message: TicketMessageEnum.SUBJECT_REQUIRED })
|
||||
@IsString({ message: TicketMessageEnum.SUBJECT_STRING })
|
||||
@Length(3, 150, { message: TicketMessageEnum.SUBJECT_LENGTH })
|
||||
@ApiProperty({ description: "Subject of the ticket", example: "Subject" })
|
||||
subject: string;
|
||||
|
||||
@IsNotEmpty({ message: TicketMessageEnum.PRIORITY_REQUIRED })
|
||||
@IsEnum(TicketPriority, { message: TicketMessageEnum.PRIORITY_INVALID })
|
||||
@ApiProperty({ enum: TicketPriority, example: TicketPriority.LOW, description: "LOW, MEDIUM, HIGH" })
|
||||
priority: TicketPriority;
|
||||
|
||||
@IsOptional()
|
||||
@IsNotEmpty({ message: TicketMessageEnum.COMPANY_ID_REQUIRED })
|
||||
@IsUUID("7", { message: TicketMessageEnum.COMPANY_ID_SHOULD_BE_UUID })
|
||||
@ApiProperty({ description: "Company ID related to the ticket", example: "550e8400-e29b-41d4-a716-446655440000" })
|
||||
companyId?: string;
|
||||
|
||||
@IsNotEmpty({ message: TicketMessageEnum.CATEGORY_ID_REQUIRED })
|
||||
@IsUUID("4", { message: TicketMessageEnum.CATEGORY_ID_SHOULD_BE_UUID })
|
||||
@ApiProperty({ description: "Category ID of the ticket", example: "123e4567-e89b-12d3-a456-426614174000" })
|
||||
categoryId: string;
|
||||
|
||||
@IsNotEmpty({ message: TicketMessageEnum.MESSAGE_REQUIRED })
|
||||
@IsString({ message: TicketMessageEnum.MESSAGE_STRING })
|
||||
@Length(5, 500, { message: TicketMessageEnum.MESSAGE_LENGTH })
|
||||
@ApiProperty({ description: "The message content of the ticket", example: "This is a detailed description of the issue." })
|
||||
message: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsNotEmpty({ message: TicketMessageEnum.ATTACHMENT_REQUIRED })
|
||||
@ArrayMinSize(1, { message: TicketMessageEnum.ATTACHMENT_REQUIRED })
|
||||
@IsUrl({ protocols: ["http", "https"], require_protocol: true }, { each: true, message: TicketMessageEnum.ATTACHMENT_SHOULD_BE_URL })
|
||||
@ApiProperty({ description: "attachment url", example: ["https://example.com/test.png"] })
|
||||
attachmentUrls?: string[];
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { IsNotEmpty, IsUUID } from "class-validator";
|
||||
|
||||
import { UserMessage } from "../../../common/enums/message.enum";
|
||||
|
||||
export class ReferTicketDto {
|
||||
@IsNotEmpty({ message: UserMessage.ADMIN_ID_REQUIRED })
|
||||
@IsUUID("4", { message: UserMessage.ADMIN_ID_SHOULD_BE_UUID })
|
||||
@ApiProperty({ description: "admin id", example: "123e4567-e89b-12d3-a456-426614174000" })
|
||||
adminId: string;
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
import { ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { IsBoolean, IsOptional, IsString } from "class-validator";
|
||||
|
||||
import { PaginationDto } from "../../../common/DTO/pagination.dto";
|
||||
|
||||
export class SearchTicketCategoryDto extends PaginationDto {
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@ApiPropertyOptional({ description: "Filter by active status", example: true })
|
||||
isActive?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional({ description: "Search query for title", example: "technical" })
|
||||
q?: string;
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
import { ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { IsEnum, IsOptional, IsUUID } from "class-validator";
|
||||
|
||||
import { PaginationDto } from "../../../common/DTO/pagination.dto";
|
||||
import { UserMessage } from "../../../common/enums/message.enum";
|
||||
import { TicketStatus } from "../enums/ticket-status.enum";
|
||||
|
||||
export class SearchTicketQueryDto extends PaginationDto {
|
||||
@IsOptional()
|
||||
@IsEnum(TicketStatus)
|
||||
@ApiPropertyOptional({ enum: TicketStatus, description: "ticket status", example: TicketStatus.PENDING })
|
||||
status?: TicketStatus;
|
||||
|
||||
@IsOptional()
|
||||
@IsUUID("4", { message: UserMessage.USER_ID_SHOULD_BE_A_UUID })
|
||||
@ApiPropertyOptional({ description: "user id", example: "123e4567-e89b-12d3-a456-426614174000" })
|
||||
userId?: string;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { OmitType, PartialType } from "@nestjs/swagger";
|
||||
|
||||
import { CreateTicketCategoryDto } from "./create-ticket-category.dto";
|
||||
|
||||
export class UpdateTicketCategoryDto extends PartialType(OmitType(CreateTicketCategoryDto, ["userGroupId"])) {}
|
||||
Executable
+1
@@ -0,0 +1 @@
|
||||
export const REFERRAL_STRATEGY = "referralStrategy";
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
import { Collection, Entity, OneToMany, Property } from "@mikro-orm/core";
|
||||
|
||||
import { Ticket } from "./ticket.entity";
|
||||
import { BaseEntity } from "../../../common/entities/base.entity";
|
||||
|
||||
@Entity()
|
||||
export class TicketCategory extends BaseEntity {
|
||||
@Property({ type: "varchar", length: 150, unique: true })
|
||||
title!: string;
|
||||
|
||||
@Property({ type: "text", nullable: true })
|
||||
description?: string;
|
||||
|
||||
@Property({ type: "boolean", default: true })
|
||||
isActive: boolean = true;
|
||||
|
||||
@OneToMany(() => Ticket, (ticket) => ticket.category)
|
||||
tickets = new Collection<Ticket>(this);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { Entity, ManyToOne, Property } from "@mikro-orm/core";
|
||||
|
||||
import { TicketMessage } from "./ticket-message.entity";
|
||||
import { BaseEntity } from "../../../common/entities/base.entity";
|
||||
|
||||
@Entity()
|
||||
export class TicketMessageAttachment extends BaseEntity {
|
||||
@Property({ type: "varchar", length: 150, nullable: true })
|
||||
attachmentUrl?: string;
|
||||
|
||||
@ManyToOne({ entity: () => TicketMessage, deleteRule: "cascade", nullable: false })
|
||||
ticketMessage!: TicketMessage;
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
import { Cascade, Collection, Entity, ManyToOne, OneToMany, Property } from "@mikro-orm/core";
|
||||
|
||||
import { TicketMessageAttachment } from "./ticket-message-attachment";
|
||||
import { Ticket } from "./ticket.entity";
|
||||
import { BaseEntity } from "../../../common/entities/base.entity";
|
||||
import { User } from "../../users/entities/user.entity";
|
||||
|
||||
@Entity()
|
||||
export class TicketMessage extends BaseEntity {
|
||||
@Property({ type: "text" })
|
||||
content!: string;
|
||||
|
||||
@OneToMany(() => TicketMessageAttachment, (attachment) => attachment.ticketMessage, { cascade: [Cascade.ALL] })
|
||||
attachments = new Collection<TicketMessageAttachment>(this);
|
||||
|
||||
@ManyToOne(() => Ticket, { deleteRule: "cascade", nullable: false })
|
||||
ticket!: Ticket;
|
||||
|
||||
@ManyToOne(() => User, { deleteRule: "cascade", nullable: false })
|
||||
author!: User;
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
import { Cascade, Collection, Entity, Enum, ManyToOne, OneToMany, Opt, Property } from "@mikro-orm/core";
|
||||
|
||||
import { TicketCategory } from "./ticket-category.entity";
|
||||
import { TicketMessage } from "./ticket-message.entity";
|
||||
import { BaseEntity } from "../../../common/entities/base.entity";
|
||||
import { Company } from "../../companies/entities/company.entity";
|
||||
import { User } from "../../users/entities/user.entity";
|
||||
import { TicketPriority } from "../enums/ticket-priority.enum";
|
||||
import { TicketStatus } from "../enums/ticket-status.enum";
|
||||
|
||||
@Entity()
|
||||
export class Ticket extends BaseEntity {
|
||||
@Property({ type: "int", autoincrement: true, persist: false })
|
||||
numericId!: number & Opt;
|
||||
|
||||
@Property({ type: "varchar", length: 150 })
|
||||
subject!: string;
|
||||
|
||||
@Enum({ items: () => TicketStatus, nativeEnumName: "ticket_status", default: TicketStatus.PENDING })
|
||||
status: TicketStatus & Opt;
|
||||
|
||||
@Enum({ items: () => TicketPriority, nativeEnumName: "ticket_priority" })
|
||||
priority!: TicketPriority;
|
||||
|
||||
@ManyToOne(() => Company, { deleteRule: "set null", nullable: true })
|
||||
company?: Company;
|
||||
|
||||
@ManyToOne(() => User, { deleteRule: "set null", nullable: true })
|
||||
assignedTo?: User;
|
||||
|
||||
@ManyToOne(() => User, { deleteRule: "cascade", nullable: false })
|
||||
user!: User;
|
||||
|
||||
@ManyToOne(() => TicketCategory, { deleteRule: "set null", nullable: true })
|
||||
category?: TicketCategory;
|
||||
|
||||
@OneToMany(() => TicketMessage, (message) => message.ticket, { cascade: [Cascade.ALL] })
|
||||
messages = new Collection<TicketMessage>(this);
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
export enum TicketPriority {
|
||||
LOW = "LOW",
|
||||
MEDIUM = "MEDIUM",
|
||||
HIGH = "HIGH",
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
export enum TicketStatus {
|
||||
PENDING = "PENDING",
|
||||
ANSWERED = "ANSWERED",
|
||||
CLOSED = "CLOSED",
|
||||
}
|
||||
+401
@@ -0,0 +1,401 @@
|
||||
import { Collection, EntityManager } from "@mikro-orm/postgresql";
|
||||
import { BadRequestException, HttpException, Injectable, InternalServerErrorException, Logger } from "@nestjs/common";
|
||||
|
||||
import { ParamDto } from "../../../common/DTO/param.dto";
|
||||
import { CommonMessage, CompanyMessage, TicketMessageEnum, UserMessage } from "../../../common/enums/message.enum";
|
||||
import { Company } from "../../companies/entities/company.entity";
|
||||
import { NotificationQueue } from "../../notifications/queue/notification.queue";
|
||||
import { User } from "../../users/entities/user.entity";
|
||||
import { RoleEnum } from "../../users/enums/role.enum";
|
||||
import { UsersService } from "../../users/services/users.service";
|
||||
import { CreateTicketCategoryDto } from "../DTO/create-ticket-category.dto";
|
||||
import { CreateTicketMessageDto } from "../DTO/create-ticket-message.dto";
|
||||
import { CreateTicketDto } from "../DTO/create-ticket.dto";
|
||||
import { ReferTicketDto } from "../DTO/refer-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 ticketsRepository: TicketsRepository,
|
||||
private readonly ticketsCategoryRepository: TicketCategoryRepository,
|
||||
private readonly ticketMessagesRepository: TicketMessagesRepository,
|
||||
private readonly usersService: UsersService,
|
||||
private readonly notificationQueue: NotificationQueue,
|
||||
private readonly em: EntityManager,
|
||||
) {}
|
||||
|
||||
//******************************** */
|
||||
async getTicketCategories() {
|
||||
const ticketCategories = await this.ticketsCategoryRepository.find({});
|
||||
return {
|
||||
ticketCategories,
|
||||
};
|
||||
}
|
||||
//******************************** */
|
||||
|
||||
async getCategoriesList(queryDto: SearchTicketCategoryDto) {
|
||||
const [categories, count] = await this.ticketsCategoryRepository.findCategoriesList(queryDto);
|
||||
|
||||
return { categories, count, paginate: true };
|
||||
}
|
||||
//******************************** */
|
||||
async createTicketCategory(createDto: CreateTicketCategoryDto) {
|
||||
const existCategory = await this.ticketsCategoryRepository.findCategoryByTitle(createDto.title);
|
||||
|
||||
if (existCategory) throw new BadRequestException(TicketMessageEnum.CATEGORY_EXIST);
|
||||
const ticketCategory = this.ticketsCategoryRepository.create({
|
||||
...createDto,
|
||||
});
|
||||
|
||||
await this.em.persistAndFlush(ticketCategory);
|
||||
|
||||
return {
|
||||
message: TicketMessageEnum.CATEGORY_CREATED,
|
||||
ticketCategory,
|
||||
};
|
||||
}
|
||||
|
||||
//******************************** */
|
||||
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.findOne({ title, id: { $ne: paramDto.id } });
|
||||
|
||||
if (existCategory) throw new BadRequestException(TicketMessageEnum.CATEGORY_EXIST);
|
||||
}
|
||||
|
||||
this.ticketsCategoryRepository.assign(category, updateCategoryDto);
|
||||
|
||||
await this.em.flush();
|
||||
|
||||
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.em.flush();
|
||||
//
|
||||
return {
|
||||
message: CommonMessage.UPDATE_SUCCESS,
|
||||
isActive: category.isActive,
|
||||
};
|
||||
}
|
||||
|
||||
//******************************** */
|
||||
|
||||
async createTicket(createDto: CreateTicketDto, userId: string) {
|
||||
const em = this.em.fork();
|
||||
|
||||
try {
|
||||
await em.begin();
|
||||
|
||||
const [user, ticketCategory] = await Promise.all([
|
||||
this.usersService.findOneByIdWithEntityManager(userId, em),
|
||||
em.findOne(TicketCategory, { id: createDto.categoryId }),
|
||||
]);
|
||||
|
||||
if (!ticketCategory) throw new BadRequestException(TicketMessageEnum.CATEGORY_NOT_FOUND);
|
||||
|
||||
let company = null;
|
||||
if (createDto.companyId) {
|
||||
company = await em.findOne(Company, { id: createDto.companyId });
|
||||
if (!company) throw new BadRequestException(CompanyMessage.COMPANY_NOT_FOUND);
|
||||
}
|
||||
|
||||
const ticketMessage = new TicketMessage();
|
||||
ticketMessage.content = createDto.message;
|
||||
ticketMessage.author = user;
|
||||
|
||||
if (createDto.attachmentUrls?.length) {
|
||||
const attachments = createDto.attachmentUrls.map((url) => {
|
||||
const attachment = new TicketMessageAttachment();
|
||||
attachment.attachmentUrl = url;
|
||||
return attachment;
|
||||
});
|
||||
ticketMessage.attachments = new Collection<TicketMessageAttachment>(ticketMessage, attachments);
|
||||
}
|
||||
|
||||
const ticket = em.create(Ticket, {
|
||||
...createDto,
|
||||
user,
|
||||
category: ticketCategory,
|
||||
messages: [ticketMessage],
|
||||
company,
|
||||
});
|
||||
|
||||
await em.persistAndFlush(ticket);
|
||||
|
||||
await this.assignTicketAndNotify(ticket, user, em);
|
||||
|
||||
await em.commit();
|
||||
|
||||
return {
|
||||
message: TicketMessageEnum.CREATED,
|
||||
ticketId: ticket.id,
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error("Error in create ticket transaction", error);
|
||||
await em.rollback();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
//******************************** */
|
||||
|
||||
async getTickets(queryDto: SearchTicketQueryDto, userId: string) {
|
||||
const [tickets, count] = await this.ticketsRepository.findTicketsList(queryDto, userId);
|
||||
|
||||
return { tickets, count, paginate: true };
|
||||
}
|
||||
//******************************** */
|
||||
async getTicketForAdmin(queryDto: SearchTicketQueryDto, adminId: string) {
|
||||
const [tickets, count] = await this.ticketsRepository.findTicketsListForAdmin(queryDto, adminId);
|
||||
|
||||
return { tickets, count, paginate: true };
|
||||
}
|
||||
|
||||
//******************************** */
|
||||
|
||||
async createTicketMessage(ticketId: string, createDto: CreateTicketMessageDto, userId: string, role: RoleEnum) {
|
||||
const em = this.em.fork();
|
||||
|
||||
try {
|
||||
await em.begin();
|
||||
|
||||
const ticket = await this.findTicket(em, ticketId, userId, role === RoleEnum.ADMIN);
|
||||
if (ticket.status === TicketStatus.CLOSED) throw new BadRequestException(TicketMessageEnum.TICKET_CLOSED);
|
||||
|
||||
if (role === RoleEnum.ADMIN && ticket.assignedTo?.id !== userId)
|
||||
throw new BadRequestException(TicketMessageEnum.TICKET_NOT_ASSIGNED_TO_USER);
|
||||
|
||||
const user = await em.findOne(User, { id: userId });
|
||||
if (!user) throw new BadRequestException(UserMessage.USER_NOT_FOUND);
|
||||
|
||||
const ticketMessage = em.create(TicketMessage, {
|
||||
content: createDto.content,
|
||||
author: user,
|
||||
ticket,
|
||||
});
|
||||
await em.persistAndFlush(ticketMessage);
|
||||
|
||||
if (createDto.attachmentUrls?.length) {
|
||||
await this.saveAttachments(em, createDto.attachmentUrls, ticketMessage);
|
||||
}
|
||||
|
||||
this.updateTicketStatus(ticket, role === RoleEnum.ADMIN);
|
||||
await em.persistAndFlush(ticket);
|
||||
|
||||
if (role === RoleEnum.ADMIN) {
|
||||
await this.sendNotificationForAdminResponse(ticket, user);
|
||||
}
|
||||
|
||||
await em.commit();
|
||||
return {
|
||||
message: TicketMessageEnum.MESSAGE_CREATED,
|
||||
ticketMessage,
|
||||
};
|
||||
} catch (error) {
|
||||
await em.rollback();
|
||||
if (error instanceof HttpException) throw error;
|
||||
throw new InternalServerErrorException(TicketMessageEnum.ERROR_IN_TICKET_MSG_CREATION);
|
||||
}
|
||||
}
|
||||
|
||||
//******************************** */
|
||||
|
||||
async getTicketMessages(ticketId: string, userId: string, role: RoleEnum) {
|
||||
let ticket = null;
|
||||
//
|
||||
if (role === RoleEnum.ADMIN) {
|
||||
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, role: RoleEnum) {
|
||||
let ticket = null;
|
||||
|
||||
if (role === RoleEnum.ADMIN) {
|
||||
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.em.flush();
|
||||
|
||||
return {
|
||||
message: TicketMessageEnum.CLOSED,
|
||||
};
|
||||
}
|
||||
//******************************** * /
|
||||
async openTicketByAdmin(ticketId: string) {
|
||||
const ticket = await this.ticketsRepository.findTicketById(ticketId);
|
||||
if (!ticket) throw new BadRequestException(TicketMessageEnum.TICKET_NOT_FOUND);
|
||||
|
||||
ticket.status = TicketStatus.PENDING;
|
||||
await this.em.flush();
|
||||
|
||||
return {
|
||||
message: TicketMessageEnum.OPENED,
|
||||
};
|
||||
}
|
||||
|
||||
//******************************** */
|
||||
async referTicket(ticketId: string, userId: string, referDto: ReferTicketDto) {
|
||||
const em = this.em.fork();
|
||||
|
||||
try {
|
||||
await em.begin();
|
||||
console.log(userId, "admin id");
|
||||
|
||||
const ticket = await this.ticketsRepository.findTicketById(ticketId);
|
||||
if (!ticket) throw new BadRequestException(TicketMessageEnum.TICKET_NOT_FOUND);
|
||||
|
||||
const admin = await this.usersService.findOneByIdWithEntityManager(referDto.adminId, em);
|
||||
|
||||
ticket.assignedTo = admin;
|
||||
await em.flush();
|
||||
|
||||
await this.notificationQueue.addAssignTicketNotificationForAdmin(admin.id, {
|
||||
ticketId: ticket.numericId.toString(),
|
||||
subject: ticket.subject,
|
||||
date: ticket.createdAt,
|
||||
userPhone: admin.phone,
|
||||
userEmail: admin.email,
|
||||
user: `${ticket.user.firstName} ${ticket.user.lastName}`,
|
||||
});
|
||||
|
||||
await em.commit();
|
||||
return {
|
||||
message: TicketMessageEnum.REFERRED,
|
||||
ticket,
|
||||
};
|
||||
} catch (error) {
|
||||
await em.rollback();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
//******************************** */
|
||||
|
||||
async getUnreadTickets() {
|
||||
const unreadTickets = await this.ticketsRepository.count({
|
||||
status: TicketStatus.PENDING,
|
||||
});
|
||||
return unreadTickets;
|
||||
}
|
||||
|
||||
//******************************** */
|
||||
async countUserTickets(userId: string) {
|
||||
const ticketCount = await this.ticketsRepository.count({
|
||||
user: { id: userId },
|
||||
status: { $ne: TicketStatus.CLOSED },
|
||||
});
|
||||
return ticketCount;
|
||||
}
|
||||
|
||||
//***************************** */
|
||||
private async assignTicketAndNotify(ticket: Ticket, user: User, em: EntityManager) {
|
||||
const assignedUser = await this.usersService.findAdminWithLeastTickets();
|
||||
//
|
||||
|
||||
if (assignedUser) {
|
||||
ticket.assignedTo = assignedUser;
|
||||
await em.persistAndFlush(ticket);
|
||||
|
||||
await this.notificationQueue.addAssignTicketNotificationForAdmin(assignedUser.id, {
|
||||
ticketId: ticket.numericId.toString(),
|
||||
subject: ticket.subject,
|
||||
date: ticket.createdAt,
|
||||
userPhone: user.phone,
|
||||
userEmail: user.email,
|
||||
user: `${user.firstName} ${user.lastName}`,
|
||||
});
|
||||
}
|
||||
|
||||
await this.notificationQueue.addTicketNotification(user.id, {
|
||||
ticketId: ticket.numericId.toString(),
|
||||
subject: ticket.subject,
|
||||
date: ticket.createdAt,
|
||||
userPhone: user.phone,
|
||||
userEmail: user.email,
|
||||
});
|
||||
}
|
||||
//***************************** */
|
||||
|
||||
private async findTicket(em: EntityManager, ticketId: string, userId: string, isAdmin: boolean): Promise<Ticket> {
|
||||
const ticket = isAdmin
|
||||
? await em.findOne(Ticket, { id: ticketId, user: { id: userId } }, { populate: ["user", "assignedTo"] })
|
||||
: await em.findOne(Ticket, { id: ticketId, user: { id: userId } }, { populate: ["user", "assignedTo"] });
|
||||
|
||||
if (!ticket) throw new BadRequestException(TicketMessageEnum.TICKET_NOT_FOUND);
|
||||
return ticket;
|
||||
}
|
||||
//***************************** */
|
||||
|
||||
private updateTicketStatus(ticket: Ticket, isAdmin: boolean): void {
|
||||
if (isAdmin && ticket.status === TicketStatus.PENDING) {
|
||||
ticket.status = TicketStatus.ANSWERED;
|
||||
} else if (!isAdmin && ticket.status === TicketStatus.ANSWERED) {
|
||||
ticket.status = TicketStatus.PENDING;
|
||||
}
|
||||
}
|
||||
//***************************** */
|
||||
|
||||
private async saveAttachments(em: EntityManager, attachmentUrls: string[], ticketMessage: TicketMessage) {
|
||||
const attachments = attachmentUrls.map((url) =>
|
||||
em.create(TicketMessageAttachment, {
|
||||
attachmentUrl: url,
|
||||
ticketMessage,
|
||||
}),
|
||||
);
|
||||
await em.persistAndFlush(attachments);
|
||||
}
|
||||
//***************************** */
|
||||
|
||||
private async sendNotificationForAdminResponse(ticket: Ticket, admin: User) {
|
||||
await this.notificationQueue.addAnswerTicketNotification(ticket.user.id, {
|
||||
ticketId: ticket.numericId.toString(),
|
||||
subject: ticket.subject,
|
||||
date: ticket.createdAt,
|
||||
userPhone: admin.phone,
|
||||
userEmail: admin.email,
|
||||
});
|
||||
}
|
||||
|
||||
//***************************** */
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { EntityRepository } from "@mikro-orm/core";
|
||||
|
||||
import { TicketMessageAttachment } from "../entities/ticket-message-attachment";
|
||||
|
||||
export class TicketMessageAttachmentsRepository extends EntityRepository<TicketMessageAttachment> {}
|
||||
@@ -0,0 +1,57 @@
|
||||
import { EntityRepository } from "@mikro-orm/postgresql";
|
||||
|
||||
import { PaginationUtils } from "../../utils/providers/pagination.utils";
|
||||
import { SearchTicketCategoryDto } from "../DTO/search-ticket-category.dto";
|
||||
import { TicketCategory } from "../entities/ticket-category.entity";
|
||||
|
||||
export class TicketCategoryRepository extends EntityRepository<TicketCategory> {
|
||||
//
|
||||
async findCategoryByTitle(title: string): Promise<TicketCategory | null> {
|
||||
return this.findOne({ title });
|
||||
}
|
||||
|
||||
async findCategoryById(id: string): Promise<TicketCategory | null> {
|
||||
return this.findOne({ id });
|
||||
}
|
||||
|
||||
async findCategoriesList(queryDto: SearchTicketCategoryDto) {
|
||||
const { limit, skip } = PaginationUtils(queryDto);
|
||||
|
||||
const qb = this.createQueryBuilder("category")
|
||||
.leftJoinAndSelect("category.group", "group")
|
||||
.select(["category.*", "group.id", "group.name"]);
|
||||
|
||||
// Add ticket count
|
||||
qb.addSelect(
|
||||
this.em
|
||||
.createQueryBuilder(TicketCategory, "c")
|
||||
.count("t.id")
|
||||
.leftJoin("c.tickets", "t")
|
||||
.where("c.id = category.id")
|
||||
.as("ticketsCount"),
|
||||
);
|
||||
|
||||
// Add users count
|
||||
qb.addSelect(
|
||||
this.em
|
||||
.createQueryBuilder(TicketCategory, "c")
|
||||
.count("u.id")
|
||||
.leftJoin("c.group", "g")
|
||||
.leftJoin("g.users", "u")
|
||||
.where("c.id = category.id")
|
||||
.as("usersCount"),
|
||||
);
|
||||
|
||||
if (queryDto.isActive !== undefined) {
|
||||
qb.andWhere({ isActive: queryDto.isActive });
|
||||
}
|
||||
|
||||
if (queryDto.q) {
|
||||
qb.andWhere({ title: { $ilike: `%${queryDto.q}%` } });
|
||||
}
|
||||
|
||||
const [items, total] = await qb.offset(skip).limit(limit).getResultAndCount();
|
||||
|
||||
return [items, total];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { EntityRepository } from "@mikro-orm/postgresql";
|
||||
|
||||
import { TicketMessage } from "../entities/ticket-message.entity";
|
||||
|
||||
export class TicketMessagesRepository extends EntityRepository<TicketMessage> {
|
||||
//
|
||||
async findMessagesByTicketId(ticketId: string) {
|
||||
return this.find(
|
||||
{ ticket: { id: ticketId } },
|
||||
{
|
||||
populate: ["author", "author.role", "attachments"],
|
||||
fields: [
|
||||
"id",
|
||||
"content",
|
||||
"createdAt",
|
||||
"updatedAt",
|
||||
"ticket",
|
||||
"author.id",
|
||||
"author.firstName",
|
||||
"author.lastName",
|
||||
"author.role.id",
|
||||
"author.role.name",
|
||||
"attachments.id",
|
||||
"attachments.attachmentUrl",
|
||||
],
|
||||
orderBy: { createdAt: "ASC" },
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
import { EntityRepository, FilterQuery } from "@mikro-orm/postgresql";
|
||||
|
||||
import { PaginationUtils } from "../../utils/providers/pagination.utils";
|
||||
import { SearchTicketQueryDto } from "../DTO/search-ticket-query.dto";
|
||||
import { Ticket } from "../entities/ticket.entity";
|
||||
import { TicketStatus } from "../enums/ticket-status.enum";
|
||||
|
||||
export class TicketsRepository extends EntityRepository<Ticket> {
|
||||
async findTicketById(id: string, userId?: string) {
|
||||
return this.findOne(
|
||||
{
|
||||
id,
|
||||
...(userId && { user: { id: userId } }),
|
||||
},
|
||||
{
|
||||
populate: ["user", "category", "assignedTo", "company"],
|
||||
fields: [
|
||||
"id",
|
||||
"numericId",
|
||||
"subject",
|
||||
"priority",
|
||||
"status",
|
||||
"createdAt",
|
||||
"user.id",
|
||||
"user.firstName",
|
||||
"user.lastName",
|
||||
"assignedTo.id",
|
||||
"assignedTo.firstName",
|
||||
"assignedTo.lastName",
|
||||
"company.id",
|
||||
"company.name",
|
||||
"category.id",
|
||||
"category.title",
|
||||
],
|
||||
},
|
||||
);
|
||||
}
|
||||
//******************************** */
|
||||
|
||||
async findTicketsList(queryDto: SearchTicketQueryDto, userId: string) {
|
||||
const { limit, skip } = PaginationUtils(queryDto);
|
||||
|
||||
const where: FilterQuery<Ticket> = { user: { id: userId } };
|
||||
if (queryDto.status) {
|
||||
where.status = queryDto.status;
|
||||
} else {
|
||||
where.status = { $ne: TicketStatus.CLOSED };
|
||||
}
|
||||
|
||||
return this.findAndCount(where, {
|
||||
populate: ["category", "company"],
|
||||
fields: [
|
||||
"id",
|
||||
"subject",
|
||||
"status",
|
||||
"priority",
|
||||
"numericId",
|
||||
"createdAt",
|
||||
"updatedAt",
|
||||
"category.id",
|
||||
"category.title",
|
||||
"company.id",
|
||||
"company.name",
|
||||
],
|
||||
orderBy: { createdAt: "DESC" },
|
||||
limit,
|
||||
offset: skip,
|
||||
});
|
||||
}
|
||||
//******************************** */
|
||||
async findTicketsListForAdmin(queryDto: SearchTicketQueryDto, adminId: string) {
|
||||
const { limit, skip } = PaginationUtils(queryDto);
|
||||
|
||||
const where: FilterQuery<Ticket> = {};
|
||||
|
||||
where.assignedTo = { id: adminId };
|
||||
|
||||
if (queryDto.status) {
|
||||
where.status = queryDto.status;
|
||||
}
|
||||
if (queryDto.userId) {
|
||||
where.user = { id: queryDto.userId };
|
||||
}
|
||||
|
||||
return this.findAndCount(where, {
|
||||
populate: ["category", "user", "assignedTo", "company"],
|
||||
fields: [
|
||||
"id",
|
||||
"subject",
|
||||
"status",
|
||||
"priority",
|
||||
"numericId",
|
||||
"createdAt",
|
||||
"updatedAt",
|
||||
"category.id",
|
||||
"category.title",
|
||||
"user.id",
|
||||
"user.firstName",
|
||||
"user.lastName",
|
||||
"assignedTo.id",
|
||||
"assignedTo.firstName",
|
||||
"assignedTo.lastName",
|
||||
"company.id",
|
||||
"company.name",
|
||||
],
|
||||
orderBy: { createdAt: "DESC" },
|
||||
limit,
|
||||
offset: skip,
|
||||
});
|
||||
}
|
||||
}
|
||||
Executable
+118
@@ -0,0 +1,118 @@
|
||||
import { Body, Controller, Get, HttpCode, HttpStatus, Param, Patch, Post, Query } from "@nestjs/common";
|
||||
import { ApiOperation, ApiTags } from "@nestjs/swagger";
|
||||
|
||||
import { CreateTicketCategoryDto } from "./DTO/create-ticket-category.dto";
|
||||
import { CreateTicketMessageDto } from "./DTO/create-ticket-message.dto";
|
||||
import { CreateTicketDto } from "./DTO/create-ticket.dto";
|
||||
import { ReferTicketDto } from "./DTO/refer-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 { TicketsService } from "./providers/tickets.service";
|
||||
import { AdminRoute } from "../../common/decorators/admin.decorator";
|
||||
import { AuthGuards } from "../../common/decorators/auth-guard.decorator";
|
||||
import { UserDec } from "../../common/decorators/user.decorator";
|
||||
import { ParamDto } from "../../common/DTO/param.dto";
|
||||
import { RoleEnum } from "../users/enums/role.enum";
|
||||
|
||||
@Controller("tickets")
|
||||
@ApiTags("Tickets")
|
||||
@AuthGuards()
|
||||
export class TicketsController {
|
||||
constructor(private readonly ticketsService: TicketsService) {}
|
||||
|
||||
//----------------- ticket categories -------------------
|
||||
|
||||
@ApiOperation({ summary: "Create ticket category => admin route" })
|
||||
@Post("category")
|
||||
createTicketCategory(@Body() createDto: CreateTicketCategoryDto) {
|
||||
return this.ticketsService.createTicketCategory(createDto);
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: "Update ticket category => admin route" })
|
||||
@Patch("category/:id")
|
||||
updateCategory(@Param() paramDto: ParamDto, @Body() updateCategoryDto: UpdateTicketCategoryDto) {
|
||||
return this.ticketsService.updateCategory(paramDto, updateCategoryDto);
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: "Get ticket categories " })
|
||||
@Get("categories")
|
||||
//
|
||||
getTicketCategories() {
|
||||
return this.ticketsService.getTicketCategories();
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: "Get ticket categories list => admin route" })
|
||||
@AdminRoute()
|
||||
@Get("categories-list")
|
||||
getCategoriesList(@Query() queryDto: SearchTicketCategoryDto) {
|
||||
return this.ticketsService.getCategoriesList(queryDto);
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: "toggle status of categories => admin route" })
|
||||
@AdminRoute()
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Post("category/toggle-status/:id")
|
||||
toggleCategoryStatus(@Param() paramDto: ParamDto) {
|
||||
return this.ticketsService.toggleCategoryStatus(paramDto);
|
||||
}
|
||||
|
||||
//----------------- ticket and ticket messages -------------------
|
||||
|
||||
@ApiOperation({ summary: "create ticket ==> user route" })
|
||||
@Post()
|
||||
createTicket(@Body() createDto: CreateTicketDto, @UserDec("id") userId: string) {
|
||||
return this.ticketsService.createTicket(createDto, userId);
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: "Get all tickets of user ==> user route" })
|
||||
@Get()
|
||||
getTickets(@Query() queryDto: SearchTicketQueryDto, @UserDec("id") userId: string) {
|
||||
return this.ticketsService.getTickets(queryDto, userId);
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: "Get all tickets ==> admin route" })
|
||||
@AdminRoute()
|
||||
@Get("admin")
|
||||
getAllTickets(@Query() queryDto: SearchTicketQueryDto, @UserDec("id") adminId: string) {
|
||||
return this.ticketsService.getTicketForAdmin(queryDto, adminId);
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: "create ticket messages" })
|
||||
@Post(":id/messages")
|
||||
createTicketMessage(
|
||||
@Param() paramDto: ParamDto,
|
||||
@Body() createDto: CreateTicketMessageDto,
|
||||
@UserDec("id") userId: string,
|
||||
@UserDec("role") role: RoleEnum,
|
||||
) {
|
||||
return this.ticketsService.createTicketMessage(paramDto.id, createDto, userId, role);
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: "Get all ticket messages of user" })
|
||||
@Get(":id/messages")
|
||||
getTicketMessages(@Param() paramDto: ParamDto, @UserDec("id") userId: string, @UserDec("role") role: RoleEnum) {
|
||||
return this.ticketsService.getTicketMessages(paramDto.id, userId, role);
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: "close ticket by user or admin" })
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Post(":id/close")
|
||||
closedTicketByUser(@Param() paramDto: ParamDto, @UserDec("id") userId: string, @UserDec("role") role: RoleEnum) {
|
||||
return this.ticketsService.closeTicketByUser(paramDto.id, userId, role);
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: "open ticket by admin" })
|
||||
@AdminRoute()
|
||||
@Post(":id/open")
|
||||
openTicketByAdmin(@Param() paramDto: ParamDto) {
|
||||
return this.ticketsService.openTicketByAdmin(paramDto.id);
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: "referer ticket ==> admin route" })
|
||||
@AdminRoute()
|
||||
@Post(":id/refer")
|
||||
referTicket(@Param() paramDto: ParamDto, @UserDec("id") userId: string, @Body() referDto: ReferTicketDto) {
|
||||
return this.ticketsService.referTicket(paramDto.id, userId, referDto);
|
||||
}
|
||||
}
|
||||
Executable
+29
@@ -0,0 +1,29 @@
|
||||
import { MikroOrmModule } from "@mikro-orm/nestjs";
|
||||
import { Module } from "@nestjs/common";
|
||||
|
||||
import { TicketCategory } from "./entities/ticket-category.entity";
|
||||
import { TicketMessage } from "./entities/ticket-message.entity";
|
||||
import { Ticket } from "./entities/ticket.entity";
|
||||
import { TicketsService } from "./providers/tickets.service";
|
||||
import { TicketCategoryRepository } from "./repositories/tickets-category.repository";
|
||||
import { TicketMessagesRepository } from "./repositories/tickets-message.repository";
|
||||
import { TicketsRepository } from "./repositories/tickets.repository";
|
||||
import { TicketsController } from "./tickets.controller";
|
||||
import { UsersModule } from "../users/users.module";
|
||||
import { TicketMessageAttachment } from "./entities/ticket-message-attachment";
|
||||
import { TicketMessageAttachmentsRepository } from "./repositories/ticket-message-attachment.repository";
|
||||
import { CompaniesModule } from "../companies/companies.module";
|
||||
import { NotificationModule } from "../notifications/notifications.module";
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
MikroOrmModule.forFeature([Ticket, TicketCategory, TicketMessageAttachment, TicketMessage]),
|
||||
UsersModule,
|
||||
NotificationModule,
|
||||
CompaniesModule,
|
||||
],
|
||||
providers: [TicketsService, TicketsRepository, TicketCategoryRepository, TicketMessagesRepository, TicketMessageAttachmentsRepository],
|
||||
controllers: [TicketsController],
|
||||
exports: [TicketsService],
|
||||
})
|
||||
export class TicketsModule {}
|
||||
@@ -43,7 +43,46 @@ export class UsersService {
|
||||
if (!user) throw new BadRequestException(UserMessage.USER_NOT_FOUND);
|
||||
return { user };
|
||||
}
|
||||
/*******************************/
|
||||
|
||||
async findOneByIdWithEntityManager(id: string, em: EntityManager) {
|
||||
const user = await em.findOne(User, { id }, { populate: ["role"] });
|
||||
if (!user) throw new BadRequestException(UserMessage.USER_NOT_FOUND);
|
||||
return user;
|
||||
}
|
||||
/*******************************/
|
||||
|
||||
async findAdminWithLeastTickets() {
|
||||
// // Find all admins with their assigned tickets
|
||||
// const admins = await this.userRepository.find(
|
||||
// { role: { name: RoleEnum.ADMIN } },
|
||||
// { populate: ['role', 'assignedTickets'] }
|
||||
// );
|
||||
|
||||
// if (!admins.length) return null;
|
||||
|
||||
// // Find the admin with the fewest assigned tickets
|
||||
// return admins.reduce((minAdmin, admin) =>
|
||||
// (minAdmin.assignedTickets.length <= admin.assignedTickets.length ? minAdmin : admin)
|
||||
// );
|
||||
const result = await this.em.getConnection().execute(
|
||||
`
|
||||
SELECT u.*
|
||||
FROM "user" u
|
||||
INNER JOIN "role" r ON u.role_id = r.id
|
||||
WHERE r.name = ?
|
||||
ORDER BY (
|
||||
SELECT COUNT(*) FROM "ticket" t WHERE t.assigned_to_id = u.id
|
||||
) ASC
|
||||
LIMIT 1
|
||||
`,
|
||||
[RoleEnum.ADMIN],
|
||||
);
|
||||
|
||||
// If you want a User entity, load it by id:
|
||||
if (result.length === 0) return null;
|
||||
return this.userRepository.findOne({ id: result[0].id }, { populate: ["role"] });
|
||||
}
|
||||
/*******************************/
|
||||
|
||||
async createUser(registerDto: CompleteRegistrationDto | CreateCompanyDto, hashedPassword: string, em: EntityManager) {
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { MikroOrmModule } from "@mikro-orm/nestjs";
|
||||
import { Module } from "@nestjs/common";
|
||||
|
||||
import { Role } from "./entities/role.entity";
|
||||
import { User } from "./entities/user.entity";
|
||||
import { UsersService } from "./services/users.service";
|
||||
import { UsersController } from "./users.controller";
|
||||
|
||||
@Module({
|
||||
imports: [MikroOrmModule.forFeature([User])],
|
||||
imports: [MikroOrmModule.forFeature([User, Role])],
|
||||
controllers: [UsersController],
|
||||
providers: [UsersService],
|
||||
exports: [UsersService],
|
||||
|
||||
Reference in New Issue
Block a user