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 { CompaniesModule } from "./modules/companies/companies.module";
|
||||||
import { IndustriesModule } from "./modules/industries/industries.module";
|
import { IndustriesModule } from "./modules/industries/industries.module";
|
||||||
import { NotificationModule } from "./modules/notifications/notifications.module";
|
import { NotificationModule } from "./modules/notifications/notifications.module";
|
||||||
|
import { TicketsModule } from "./modules/tickets/tickets.module";
|
||||||
import { UploaderModule } from "./modules/uploader/uploader.module";
|
import { UploaderModule } from "./modules/uploader/uploader.module";
|
||||||
import { UsersModule } from "./modules/users/users.module";
|
import { UsersModule } from "./modules/users/users.module";
|
||||||
import { UtilsModule } from "./modules/utils/utils.module";
|
import { UtilsModule } from "./modules/utils/utils.module";
|
||||||
@@ -46,6 +47,7 @@ import { UtilsModule } from "./modules/utils/utils.module";
|
|||||||
IndustriesModule,
|
IndustriesModule,
|
||||||
CompaniesModule,
|
CompaniesModule,
|
||||||
UploaderModule,
|
UploaderModule,
|
||||||
|
TicketsModule,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class AppModule implements NestModule {
|
export class AppModule implements NestModule {
|
||||||
|
|||||||
@@ -259,8 +259,8 @@ export const enum TicketMessageEnum {
|
|||||||
PRIORITY_INVALID = "اولویت تیکت باید یکی از مقادیر LOW، MEDIUM یا HIGH باشد",
|
PRIORITY_INVALID = "اولویت تیکت باید یکی از مقادیر LOW، MEDIUM یا HIGH باشد",
|
||||||
CATEGORY_ID_REQUIRED = "شناسه دستهبندی نمیتواند خالی باشد",
|
CATEGORY_ID_REQUIRED = "شناسه دستهبندی نمیتواند خالی باشد",
|
||||||
CATEGORY_ID_SHOULD_BE_UUID = "شناسه دستهبندی باید یک UUID معتبر باشد",
|
CATEGORY_ID_SHOULD_BE_UUID = "شناسه دستهبندی باید یک UUID معتبر باشد",
|
||||||
DANAK_SERVICE_ID_REQUIRED = "شناسه سرویس داناک اجباری است",
|
COMPANY_ID_REQUIRED = "شناسه شرکت اجباری است",
|
||||||
DANAK_SERVICE_ID_SHOULD_BE_UUID = "شناسه سرویس داناک باید یک UUID معتبر باشد",
|
COMPANY_ID_SHOULD_BE_UUID = "شناسه شرکت باید یک UUID معتبر باشد",
|
||||||
MESSAGE_REQUIRED = "متن پیام تیکت اجباری است",
|
MESSAGE_REQUIRED = "متن پیام تیکت اجباری است",
|
||||||
MESSAGE_STRING = "متن پیام باید یک رشته باشد",
|
MESSAGE_STRING = "متن پیام باید یک رشته باشد",
|
||||||
MESSAGE_LENGTH = "متن پیام باید بین ۵ تا ۵۰۰ کاراکتر باشد",
|
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 { BadRequestException, Injectable } from "@nestjs/common";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
|
|
||||||
@@ -28,7 +28,7 @@ export class CompaniesService {
|
|||||||
async create(createDto: CreateCompanyDto) {
|
async create(createDto: CreateCompanyDto) {
|
||||||
const em = this.em.fork();
|
const em = this.em.fork();
|
||||||
try {
|
try {
|
||||||
em.begin();
|
await em.begin();
|
||||||
|
|
||||||
const industry = await em.findOne(Industry, { id: createDto.industryId, deletedAt: null });
|
const industry = await em.findOne(Industry, { id: createDto.industryId, deletedAt: null });
|
||||||
if (!industry) throw new BadRequestException(IndustryMessage.INDUSTRY_NOT_FOUND);
|
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 hashedPassword = await this.passwordService.hashPassword(createDto.password);
|
||||||
const user = await this.usersService.createUser(createDto, hashedPassword, em);
|
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 }));
|
const services = createDto.services.map((serviceData) => {
|
||||||
company.products = new Collection(products);
|
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 }));
|
const company = em.create(Company, {
|
||||||
company.services = new Collection(services);
|
...createDto,
|
||||||
|
industry,
|
||||||
|
status: CompanyStatus.APPROVED,
|
||||||
|
products,
|
||||||
|
services,
|
||||||
|
user,
|
||||||
|
});
|
||||||
|
|
||||||
await em.persistAndFlush(company);
|
await em.persistAndFlush(company);
|
||||||
await em.commit();
|
await em.commit();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
message: CompanyMessage.COMPANY_CREATED_SUCCESSFULLY,
|
message: CompanyMessage.COMPANY_CREATED_SUCCESSFULLY,
|
||||||
company,
|
company: company.name,
|
||||||
user,
|
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await em.rollback();
|
await em.rollback();
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//-----------------------------------
|
//-----------------------------------
|
||||||
|
|
||||||
async updateCompanyById(id: string, updateDto: UpdateCompanyDto) {
|
async updateCompanyById(id: string, updateDto: UpdateCompanyDto) {
|
||||||
const em = this.em.fork();
|
const em = this.em.fork();
|
||||||
try {
|
try {
|
||||||
em.begin();
|
await em.begin();
|
||||||
|
|
||||||
const company = await em.findOne(Company, { id, deletedAt: null }, { populate: ["user"] });
|
const company = await em.findOne(Company, { id, deletedAt: null }, { populate: ["user"] });
|
||||||
if (!company) throw new BadRequestException(CompanyMessage.COMPANY_NOT_FOUND);
|
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 { Body, Controller, Delete, Get, Param, Patch, Post, Query } from "@nestjs/common";
|
||||||
|
import { ApiOperation } from "@nestjs/swagger";
|
||||||
|
|
||||||
import { CreateIndustryDto } from "./DTO/create-industry.dto";
|
import { CreateIndustryDto } from "./DTO/create-industry.dto";
|
||||||
import { IndustryListQueryDto } from "./DTO/industry-list-query.dto";
|
import { IndustryListQueryDto } from "./DTO/industry-list-query.dto";
|
||||||
@@ -11,31 +12,37 @@ export class IndustriesController {
|
|||||||
constructor(private readonly industriesService: IndustriesService) {}
|
constructor(private readonly industriesService: IndustriesService) {}
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
|
@ApiOperation({ summary: "create industry" })
|
||||||
create(@Body() createIndustryDto: CreateIndustryDto) {
|
create(@Body() createIndustryDto: CreateIndustryDto) {
|
||||||
return this.industriesService.create(createIndustryDto);
|
return this.industriesService.create(createIndustryDto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get(":id")
|
@Get(":id")
|
||||||
|
@ApiOperation({ summary: "get industry by id" })
|
||||||
getIndustryById(@Param() paramDto: ParamDto) {
|
getIndustryById(@Param() paramDto: ParamDto) {
|
||||||
return this.industriesService.getIndustriesById(paramDto.id);
|
return this.industriesService.getIndustriesById(paramDto.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Patch(":id")
|
@Patch(":id")
|
||||||
|
@ApiOperation({ summary: "update industry" })
|
||||||
updateIndustry(@Param() paramDto: ParamDto, @Body() updateIndustryDto: UpdateIndustryDto) {
|
updateIndustry(@Param() paramDto: ParamDto, @Body() updateIndustryDto: UpdateIndustryDto) {
|
||||||
return this.industriesService.updateIndustry(paramDto.id, updateIndustryDto);
|
return this.industriesService.updateIndustry(paramDto.id, updateIndustryDto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Delete(":id")
|
@Delete(":id")
|
||||||
|
@ApiOperation({ summary: "delete industry" })
|
||||||
deleteIndustry(@Param() paramDto: ParamDto) {
|
deleteIndustry(@Param() paramDto: ParamDto) {
|
||||||
return this.industriesService.deleteIndustry(paramDto.id);
|
return this.industriesService.deleteIndustry(paramDto.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get("list")
|
@Get("list")
|
||||||
|
@ApiOperation({ summary: "get industries list for admin" })
|
||||||
getIndustriesForAdmin(@Query() query: IndustryListQueryDto) {
|
getIndustriesForAdmin(@Query() query: IndustryListQueryDto) {
|
||||||
return this.industriesService.getIndustriesListForAdmin(query);
|
return this.industriesService.getIndustriesListForAdmin(query);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Patch(":id/toggle-status")
|
@Patch(":id/toggle-status")
|
||||||
|
@ApiOperation({ summary: "toggle industry status" })
|
||||||
toggleStatus(@Param() paramDto: ParamDto) {
|
toggleStatus(@Param() paramDto: ParamDto) {
|
||||||
return this.industriesService.toggleStatus(paramDto.id);
|
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);
|
if (!user) throw new BadRequestException(UserMessage.USER_NOT_FOUND);
|
||||||
return { user };
|
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) {
|
async createUser(registerDto: CompleteRegistrationDto | CreateCompanyDto, hashedPassword: string, em: EntityManager) {
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import { MikroOrmModule } from "@mikro-orm/nestjs";
|
import { MikroOrmModule } from "@mikro-orm/nestjs";
|
||||||
import { Module } from "@nestjs/common";
|
import { Module } from "@nestjs/common";
|
||||||
|
|
||||||
|
import { Role } from "./entities/role.entity";
|
||||||
import { User } from "./entities/user.entity";
|
import { User } from "./entities/user.entity";
|
||||||
import { UsersService } from "./services/users.service";
|
import { UsersService } from "./services/users.service";
|
||||||
import { UsersController } from "./users.controller";
|
import { UsersController } from "./users.controller";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [MikroOrmModule.forFeature([User])],
|
imports: [MikroOrmModule.forFeature([User, Role])],
|
||||||
controllers: [UsersController],
|
controllers: [UsersController],
|
||||||
providers: [UsersService],
|
providers: [UsersService],
|
||||||
exports: [UsersService],
|
exports: [UsersService],
|
||||||
|
|||||||
Reference in New Issue
Block a user