From 1d115093710228e0e6cfabddce6745c994f07839 Mon Sep 17 00:00:00 2001 From: mahyargdz Date: Tue, 28 Jan 2025 20:08:43 +0330 Subject: [PATCH] chore: ticket service but not tested --- src/app.module.ts | 4 +- src/common/enums/message.enum.ts | 19 ++++ .../interceptors/pagination.interceptor.ts | 2 +- src/main.ts | 6 +- .../announcements/announcement.module.ts | 2 +- .../danak-services.controller.ts | 20 +++- .../entities/danak-service.entity.ts | 18 +++- .../providers/danak-services.service.ts | 23 ++++- .../repositories/danak-services.repository.ts | 4 + .../tickets/DTO/create-ticket-category.dto.ts | 28 ++++++ .../tickets/DTO/create-ticket-service.dto.ts | 29 ++++++ .../tickets/DTO/search-ticket-category.dto.ts | 3 + .../tickets/DTO/update-ticket-category.dto.ts | 5 + .../entities/ticket-category.entity.ts | 11 ++- .../tickets/entities/ticket-message.entity.ts | 3 +- src/modules/tickets/entities/ticket.entity.ts | 4 +- .../tickets/providers/tickets.service.ts | 97 ++++++++++++++++++- .../tickets-category.repository.ts | 11 ++- .../tickets-message.repository.ts | 3 +- .../repositories/tickets.repository.ts | 5 +- src/modules/tickets/tickets.controller.ts | 58 ++++++++++- src/modules/tickets/tickets.module.ts | 11 ++- src/modules/users/DTO/user-group.dto.ts | 12 +-- .../users/entities/user-group.entity.ts | 10 +- src/modules/users/entities/user.entity.ts | 9 +- src/modules/users/providers/users.service.ts | 35 +++---- .../repositories/user-group.repository.ts | 4 + src/modules/users/users.controller.ts | 5 +- 28 files changed, 368 insertions(+), 73 deletions(-) create mode 100644 src/modules/tickets/DTO/create-ticket-category.dto.ts create mode 100644 src/modules/tickets/DTO/create-ticket-service.dto.ts create mode 100644 src/modules/tickets/DTO/search-ticket-category.dto.ts create mode 100644 src/modules/tickets/DTO/update-ticket-category.dto.ts diff --git a/src/app.module.ts b/src/app.module.ts index 43e45d8..c518487 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -9,7 +9,7 @@ import { cacheConfig } from "./configs/cache.config"; import { rateLimitConfig } from "./configs/rateLimit.config"; import { databaseConfigs } from "./configs/typeorm.config"; import { HTTPLogger } from "./core/middlewares/logger.middleware"; -import { AnnoucementsModule } from "./modules/announcements/announcement.module"; +import { AnnouncementsModule } from "./modules/announcements/announcement.module"; import { AuthModule } from "./modules/auth/auth.module"; import { CriticismModule } from "./modules/criticisms/criticisms.module"; import { DanakServicesModule } from "./modules/danak-services/danak-services.module"; @@ -28,7 +28,7 @@ import { UsersModule } from "./modules/users/users.module"; TicketsModule, AuthModule, DanakServicesModule, - AnnoucementsModule, + AnnouncementsModule, UploaderModule, CriticismModule, ], diff --git a/src/common/enums/message.enum.ts b/src/common/enums/message.enum.ts index 2ca536e..94f9cbd 100644 --- a/src/common/enums/message.enum.ts +++ b/src/common/enums/message.enum.ts @@ -60,6 +60,8 @@ export const enum UserMessage { USERNAME_EXIST = "نام کاربری قبلا ثبت شده است", USERNAME_NOT_EMPTY = "نام کاربری نمی‌تواند خالی باشد", USERNAME_SHOULD_BE_BETWEEN_3_AND_50 = "نام کاربری باید بین ۳ تا ۵۰ کاراکتر باشد", + USER_GROUP_NOT_FOUND = "گروه کاربری یافت نشد", + USER_GROUP_CREATED = "گروه کاربری با موفقیت ایجاد شد", } export const enum CommonMessage { @@ -116,6 +118,8 @@ export const enum ServiceMessage { DESCRIPTION_LENGTH = "توضیحات سرویس باید بین ۱۰ تا ۵۰۰ کاراکتر باشد", SOFTWARE_LANGUAGE_LENGTH = "زبان نرم افزار سرویس باید بین ۳ تا ۱۵۰ کاراکتر باشد", AUTHOR_LENGTH = "نویسنده سرویس باید بین 3 تا 100 کاراکتر باشد", + SERVICE_NOT_EXIST = "سرویس مورد نظر یافت نشد", + NAME_EXIST = "سرویس با این نام قبلا ثبت شده است", } export const enum AnnouncementMessage { @@ -134,3 +138,18 @@ export const enum CriticismMessage { NOT_FOUND = "انتقادی با این ایدی یافت نشد", SEARCH_QUERY_STRING = "رشته جستجو باید یک رشته باشد", } +export const enum TicketMessageEnum { + TITLE_REQUIRED = "عنوان تیکت مورد نیاز است", + TITLE_STRING = "عنوان تیکت باید یک رشته باشد", + TITLE_LENGTH = "عنوان تیکت باید بین ۳ تا ۱۵۰ کاراکتر باشد", + USER_GROUP_ID_NOT_EMPTY = "شناسه گروه کاربری نمی‌تواند خالی باشد", + DESCRIPTION_REQUIRED = "توضیحات تیکت مورد نیاز است", + DESCRIPTION_STRING = "توضیحات تیکت باید یک رشته باشد", + DESCRIPTION_LENGTH = "توضیحات تیکت باید بین ۳ تا ۵۰۰ کاراکتر باشد", + USER_GROUP_ID_SHOULD_BE_UUID = "شناسه گروه کاربری باید یک یو یو آی دی باشد", + IS_ACTIVE_REQUIRED = "وضعیت تیکت مورد نیاز است", + IS_ACTIVE_SHOULD_BE_BOOLEAN = "وضعیت تیکت باید یک بولین باشد", + CATEGORY_EXIST = "دسته بندی با این نام قبلا ثبت شده است", + CATEGORY_CREATED = "دسته بندی با موفقیت ایجاد شد", + CATEGORY_NOT_FOUND = "دسته بندی یافت نشد", +} diff --git a/src/core/interceptors/pagination.interceptor.ts b/src/core/interceptors/pagination.interceptor.ts index 6f8b7b4..ef4002d 100644 --- a/src/core/interceptors/pagination.interceptor.ts +++ b/src/core/interceptors/pagination.interceptor.ts @@ -20,7 +20,7 @@ export class PaginationInterceptor implements NestInterceptor { return next.handle().pipe( map((data) => { - if (data.paginate || data.count) { + if (data && (data.paginate || data.count)) { const { count, ...response } = data; this.logger.log(`paginate response from ${className}.${handlerName}`); const pager = this.formatPage(page, limit, count, request); diff --git a/src/main.ts b/src/main.ts index f30c18e..76f7ea4 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,6 +1,6 @@ -import { Logger, ValidationPipe } from "@nestjs/common"; +import { ClassSerializerInterceptor, Logger, ValidationPipe } from "@nestjs/common"; import { ConfigService } from "@nestjs/config"; -import { NestFactory } from "@nestjs/core"; +import { NestFactory, Reflector } from "@nestjs/core"; import { FastifyAdapter, NestFastifyApplication } from "@nestjs/platform-fastify"; import { AppModule } from "./app.module"; @@ -13,7 +13,7 @@ async function bootstrap() { const app = await NestFactory.create(AppModule, new FastifyAdapter()); app.useGlobalPipes(new ValidationPipe({ transform: true, whitelist: true })); - app.useGlobalInterceptors(new ResponseInterceptor(), new PaginationInterceptor()); + app.useGlobalInterceptors(new ResponseInterceptor(), new PaginationInterceptor(), new ClassSerializerInterceptor(app.get(Reflector))); app.useGlobalFilters(new HttpExceptionFilter()); app.enableCors({ diff --git a/src/modules/announcements/announcement.module.ts b/src/modules/announcements/announcement.module.ts index cb648ed..3d91038 100644 --- a/src/modules/announcements/announcement.module.ts +++ b/src/modules/announcements/announcement.module.ts @@ -11,4 +11,4 @@ import { Announcement } from "./entities/announcement.entity"; controllers: [AnnouncementController], exports: [TypeOrmModule], }) -export class AnnoucementsModule {} +export class AnnouncementsModule {} diff --git a/src/modules/danak-services/danak-services.controller.ts b/src/modules/danak-services/danak-services.controller.ts index 7e3bd1f..4316409 100644 --- a/src/modules/danak-services/danak-services.controller.ts +++ b/src/modules/danak-services/danak-services.controller.ts @@ -8,7 +8,9 @@ import { DanakServicesService } from "./providers/danak-services.service"; import { AuthGuards } from "../../common/decorators/auth-guard.decorator"; import { Pagination } from "../../common/decorators/pagination.decorator"; import { Roles } from "../../common/decorators/roles.decorator"; +import { UserDec } from "../../common/decorators/user.decorator"; import { ParamDto } from "../../common/DTO/param.dto"; +import { User } from "../users/entities/user.entity"; import { RoleEnum } from "../users/enums/role.enum"; @Controller("services") @@ -19,7 +21,7 @@ export class DanakServicesController { // @AuthGuards() @Roles(RoleEnum.ADMIN) - @ApiOperation({ summary: "Create a new service category" }) + @ApiOperation({ summary: "Create a new service category => admin route" }) @Post("category") createCategory(@Body() createDto: CreateCategoryDto) { return this.danakServicesService.createCategory(createDto); @@ -27,7 +29,7 @@ export class DanakServicesController { @AuthGuards() @Roles(RoleEnum.ADMIN) - @ApiOperation({ summary: "Get all service categories" }) + @ApiOperation({ summary: "Get all service categories => admin route" }) @Get("category") getCategories(@Query() queryDto: CategorySearchQueryDto) { return this.danakServicesService.getCategories(queryDto); @@ -35,7 +37,7 @@ export class DanakServicesController { @AuthGuards() @Roles(RoleEnum.ADMIN) - @ApiOperation({ summary: "Get all service categories" }) + @ApiOperation({ summary: "Get all service categories => admin route" }) @Pagination() @Get("category-list") getCategoryList(@Query() queryDto: CategoryListSearchQueryDto) { @@ -44,7 +46,7 @@ export class DanakServicesController { @AuthGuards() @Roles(RoleEnum.ADMIN) - @ApiOperation({ summary: "toggle status of categories" }) + @ApiOperation({ summary: "toggle status of categories => admin route" }) @HttpCode(HttpStatus.OK) @Post("category/toggle-status/:id") toggleCategoryStatus(@Param() paramDto: ParamDto) { @@ -53,9 +55,17 @@ export class DanakServicesController { @AuthGuards() @Roles(RoleEnum.ADMIN) - @ApiOperation({ summary: "create new danak services" }) + @ApiOperation({ summary: "create new danak services => admin route" }) @Post() createService(@Body() createDto: CreateServiceDto) { return this.danakServicesService.createService(createDto); } + + @AuthGuards() + @Roles(RoleEnum.USER) + @ApiOperation({ summary: "get all user purchased danak services" }) + @Get("user-services") + getUserServices(@UserDec() user: User) { + return this.danakServicesService.getUserServices(user.id); + } } diff --git a/src/modules/danak-services/entities/danak-service.entity.ts b/src/modules/danak-services/entities/danak-service.entity.ts index 0c56cb3..ccf5a38 100644 --- a/src/modules/danak-services/entities/danak-service.entity.ts +++ b/src/modules/danak-services/entities/danak-service.entity.ts @@ -1,14 +1,16 @@ -import { Column, Entity, ManyToOne, OneToMany } from "typeorm"; +import { Column, Entity, ManyToMany, ManyToOne, OneToMany } from "typeorm"; import { DanakServiceCategory } from "./danak-service-category.entity"; import { DanakServiceImage } from "./danak-service-image.entity"; import { BaseEntity } from "../../../common/entities/base.entity"; +import { Ticket } from "../../tickets/entities/ticket.entity"; +import { User } from "../../users/entities/user.entity"; import { ServiceStatus } from "../enums/service-status.enum"; import { ServicesLanguage } from "../enums/services-language.enum"; @Entity() export class DanakService extends BaseEntity { - @Column({ type: "varchar", length: 100, nullable: false }) + @Column({ type: "varchar", length: 100, nullable: false, unique: true }) name: string; @Column({ type: "boolean", nullable: false, default: false }) @@ -44,12 +46,18 @@ export class DanakService extends BaseEntity { @Column({ type: "varchar", length: 255, nullable: false }) icon: string; - @OneToMany(() => DanakServiceImage, (danakServiceImage) => danakServiceImage.danakService, { eager: true, cascade: true }) - images: DanakServiceImage[]; - @ManyToOne(() => DanakServiceCategory, (danakServiceCategory) => danakServiceCategory.danakService, { nullable: false, onDelete: "RESTRICT", }) category: DanakServiceCategory; + + @OneToMany(() => DanakServiceImage, (danakServiceImage) => danakServiceImage.danakService, { eager: true, cascade: true }) + images: DanakServiceImage[]; + + @OneToMany(() => Ticket, (ticket) => ticket.danakService) + tickets: Ticket[]; + + @ManyToMany(() => User, (user) => user.danakServices) + users: User[]; } diff --git a/src/modules/danak-services/providers/danak-services.service.ts b/src/modules/danak-services/providers/danak-services.service.ts index e948a53..a88d179 100644 --- a/src/modules/danak-services/providers/danak-services.service.ts +++ b/src/modules/danak-services/providers/danak-services.service.ts @@ -2,7 +2,7 @@ import { BadRequestException, Injectable } from "@nestjs/common"; import { FindOptionsWhere, IsNull } from "typeorm"; import { ParamDto } from "../../../common/DTO/param.dto"; -import { CategoryMessage, CommonMessage } from "../../../common/enums/message.enum"; +import { CategoryMessage, CommonMessage, ServiceMessage } from "../../../common/enums/message.enum"; import { PaginationUtils } from "../../utils/providers/pagination.utils"; import { CategoryListSearchQueryDto, CategorySearchQueryDto } from "../DTO/category-search-query.dto"; import { CreateCategoryDto } from "../DTO/create-category.dto"; @@ -78,6 +78,17 @@ export class DanakServicesService { } /******************************************** */ + async getUserServices(userId: string) { + const userServices = await this.danakServicesRepository.find({ + where: { users: { id: userId } }, + }); + + return { + userServices, + }; + } + /******************************************** */ + async toggleCategoryStatus(paramDto: ParamDto) { const category = await this.danakServicesCategoryRepository.findOneById(paramDto.id); if (!category) throw new BadRequestException(CategoryMessage.CATEGORY_NOT_EXIST); @@ -93,6 +104,9 @@ export class DanakServicesService { const category = await this.danakServicesCategoryRepository.findOneById(createDto.categoryId); if (!category) throw new BadRequestException(CategoryMessage.CATEGORY_NOT_EXIST); + const existService = await this.danakServicesRepository.findOneByName(createDto.name); + if (existService) throw new BadRequestException(ServiceMessage.NAME_EXIST); + const images = createDto.images.map((image) => { return { imageUrl: image }; }); @@ -104,6 +118,13 @@ export class DanakServicesService { service, }; } + /******************************************** */ + + async getServiceByName(name: string) { + const danakService = await this.danakServicesRepository.findOneByName(name); + if (!danakService) throw new BadRequestException(ServiceMessage.SERVICE_NOT_EXIST); + return danakService; + } /******************************************** */ /******************************************** */ diff --git a/src/modules/danak-services/repositories/danak-services.repository.ts b/src/modules/danak-services/repositories/danak-services.repository.ts index d64bcaf..646ec4c 100644 --- a/src/modules/danak-services/repositories/danak-services.repository.ts +++ b/src/modules/danak-services/repositories/danak-services.repository.ts @@ -9,4 +9,8 @@ export class DanakServicesRepository extends Repository { constructor(@InjectRepository(DanakService) danakServicesRepository: Repository) { super(danakServicesRepository.target, danakServicesRepository.manager, danakServicesRepository.queryRunner); } + + async findOneByName(name: string): Promise { + return this.findOneBy({ name }); + } } diff --git a/src/modules/tickets/DTO/create-ticket-category.dto.ts b/src/modules/tickets/DTO/create-ticket-category.dto.ts new file mode 100644 index 0000000..ab4dc2e --- /dev/null +++ b/src/modules/tickets/DTO/create-ticket-category.dto.ts @@ -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; +} diff --git a/src/modules/tickets/DTO/create-ticket-service.dto.ts b/src/modules/tickets/DTO/create-ticket-service.dto.ts new file mode 100644 index 0000000..05b1e9e --- /dev/null +++ b/src/modules/tickets/DTO/create-ticket-service.dto.ts @@ -0,0 +1,29 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { IsEnum, IsNotEmpty, IsString, Length } from "class-validator"; + +import { TicketPriority } from "../enums/ticket-priority.enum"; + +export class CreateTicketDto { + @IsNotEmpty() + @IsString() + @Length(3, 150) + @ApiProperty() + title: string; + + @IsNotEmpty() + @IsString() + @Length(3, 150) + @ApiProperty() + subject: string; + + @IsNotEmpty() + @IsEnum(TicketPriority) + @ApiProperty({ enum: TicketPriority, example: TicketPriority.LOW, description: "LOW, MEDIUM, HIGH" }) + priority: TicketPriority; + + danakServiceId: string; + + categoryId: string; + + message: string; +} diff --git a/src/modules/tickets/DTO/search-ticket-category.dto.ts b/src/modules/tickets/DTO/search-ticket-category.dto.ts new file mode 100644 index 0000000..17e0593 --- /dev/null +++ b/src/modules/tickets/DTO/search-ticket-category.dto.ts @@ -0,0 +1,3 @@ +import { CategoryListSearchQueryDto } from "../../danak-services/DTO/category-search-query.dto"; + +export class SearchTicketCategoryDto extends CategoryListSearchQueryDto {} diff --git a/src/modules/tickets/DTO/update-ticket-category.dto.ts b/src/modules/tickets/DTO/update-ticket-category.dto.ts new file mode 100644 index 0000000..068cfaa --- /dev/null +++ b/src/modules/tickets/DTO/update-ticket-category.dto.ts @@ -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"])) {} diff --git a/src/modules/tickets/entities/ticket-category.entity.ts b/src/modules/tickets/entities/ticket-category.entity.ts index 47e110a..6638b39 100644 --- a/src/modules/tickets/entities/ticket-category.entity.ts +++ b/src/modules/tickets/entities/ticket-category.entity.ts @@ -1,16 +1,23 @@ -import { Column, Entity, OneToMany } from "typeorm"; +import { Column, Entity, ManyToOne, OneToMany } from "typeorm"; import { Ticket } from "./ticket.entity"; import { BaseEntity } from "../../../common/entities/base.entity"; +import { UserGroup } from "../../users/entities/user-group.entity"; @Entity() export class TicketCategory extends BaseEntity { @Column({ type: "varchar", length: 150, nullable: false, unique: true }) - name: string; + title: string; @Column({ type: "text", nullable: true }) description: string; + @Column({ type: "boolean", default: true }) + isActive: boolean; + @OneToMany(() => Ticket, (ticket) => ticket.category) tickets: Ticket[]; + + @ManyToOne(() => UserGroup, (group) => group.ticketCategories, { onDelete: "RESTRICT" }) + group: UserGroup; } diff --git a/src/modules/tickets/entities/ticket-message.entity.ts b/src/modules/tickets/entities/ticket-message.entity.ts index 29cf2ce..66df757 100644 --- a/src/modules/tickets/entities/ticket-message.entity.ts +++ b/src/modules/tickets/entities/ticket-message.entity.ts @@ -1,6 +1,7 @@ import { Column, Entity, JoinColumn, ManyToOne } from "typeorm"; -import { BaseEntity } from "../../../common/entities/base.entity"; + import { Ticket } from "./ticket.entity"; +import { BaseEntity } from "../../../common/entities/base.entity"; import { User } from "../../users/entities/user.entity"; @Entity() diff --git a/src/modules/tickets/entities/ticket.entity.ts b/src/modules/tickets/entities/ticket.entity.ts index 7b0376e..9c269a8 100644 --- a/src/modules/tickets/entities/ticket.entity.ts +++ b/src/modules/tickets/entities/ticket.entity.ts @@ -3,6 +3,7 @@ import { Column, Entity, JoinColumn, ManyToOne, OneToMany } from "typeorm"; import { TicketCategory } from "./ticket-category.entity"; import { TicketMessage } from "./ticket-message.entity"; import { BaseEntity } from "../../../common/entities/base.entity"; +import { DanakService } from "../../danak-services/entities/danak-service.entity"; import { User } from "../../users/entities/user.entity"; import { TicketPriority } from "../enums/ticket-priority.enum"; import { TicketStatus } from "../enums/ticket-status.enum"; @@ -24,7 +25,8 @@ export class Ticket extends BaseEntity { @Column({ type: "enum", enum: TicketPriority, nullable: false }) priority: TicketPriority; - //TODO:ADD service reference here + @ManyToOne(() => DanakService, (danakService) => danakService.tickets, { onDelete: "RESTRICT", nullable: false }) + danakService: DanakService; @JoinColumn() @ManyToOne(() => User, (user) => user.tickets, { nullable: true }) diff --git a/src/modules/tickets/providers/tickets.service.ts b/src/modules/tickets/providers/tickets.service.ts index 4e51b04..ad3a431 100644 --- a/src/modules/tickets/providers/tickets.service.ts +++ b/src/modules/tickets/providers/tickets.service.ts @@ -1,4 +1,97 @@ -import { Injectable } from "@nestjs/common"; +import { BadRequestException, Injectable } from "@nestjs/common"; +import { Not } from "typeorm"; + +import { ParamDto } from "../../../common/DTO/param.dto"; +import { CommonMessage, TicketMessageEnum } from "../../../common/enums/message.enum"; +import { UsersService } from "../../users/providers/users.service"; +import { PaginationUtils } from "../../utils/providers/pagination.utils"; +import { CreateTicketCategoryDto } from "../DTO/create-ticket-category.dto"; +import { SearchTicketCategoryDto } from "../DTO/search-ticket-category.dto"; +import { UpdateTicketCategoryDto } from "../DTO/update-ticket-category.dto"; +import { TicketCategoryRepository } from "../repositories/tickets-category.repository"; @Injectable() -export class TicketsService {} +export class TicketsService { + constructor( + private readonly ticketsCategoryRepository: TicketCategoryRepository, + private readonly usersService: UsersService, + ) {} + //******************************** */ + async getTicketCategories() { + const ticketCategories = await this.ticketsCategoryRepository.find({ relations: { group: true } }); + return { + ticketCategories, + }; + } + //******************************** */ + + async getCategoriesList(queryDto: SearchTicketCategoryDto) { + const { limit, skip } = PaginationUtils(queryDto); + const queryBuilder = this.ticketsCategoryRepository + .createQueryBuilder("category") + // .leftJoinAndSelect("category.group", "group") + // .addSelect(["group.id", "group.name"]) + .leftJoin("category.group", "group") + .addSelect(["group.id", "group.name"]) + // .leftJoinAndSelect("group.users", "users") + .loadRelationCountAndMap("category.ticketsCount", "category.tickets") + .loadRelationCountAndMap("category.usersCount", "group.users"); + + if (queryDto.isActive !== undefined) { + queryBuilder.andWhere("category.isActive = :isActive", { isActive: queryDto.isActive === 1 }); + } + + if (queryDto.q) { + queryBuilder.where("category.title LIKE :q", { q: `%${queryDto.q}%` }); + } + + const [categories, count] = await queryBuilder.skip(skip).take(limit).getManyAndCount(); + + return { categories, count }; + } + //******************************** */ + async createTicketCategory(createDto: CreateTicketCategoryDto) { + const { userGroup } = await this.usersService.getUserGroupById(createDto.userGroupId); + const existCategory = await this.ticketsCategoryRepository.findCategoryByTitle(createDto.title); + + if (existCategory) throw new BadRequestException(TicketMessageEnum.CATEGORY_EXIST); + const ticketCategory = this.ticketsCategoryRepository.create({ ...createDto, group: userGroup }); + await this.ticketsCategoryRepository.save(ticketCategory); + + return { + message: TicketMessageEnum.CATEGORY_CREATED, + }; + } + + //******************************** */ + async updateCategory(paramDto: ParamDto, updateCategoryDto: UpdateTicketCategoryDto) { + const category = await this.ticketsCategoryRepository.findCategoryById(paramDto.id); + if (!category) throw new BadRequestException(TicketMessageEnum.CATEGORY_NOT_FOUND); + + if (updateCategoryDto.title) { + const { title } = updateCategoryDto; + const existCategory = await this.ticketsCategoryRepository.findOneBy({ title, id: Not(paramDto.id) }); + if (existCategory) throw new BadRequestException(TicketMessageEnum.CATEGORY_EXIST); + } + await this.ticketsCategoryRepository.save({ ...category, ...updateCategoryDto }); + + return { + message: CommonMessage.UPDATE_SUCCESS, + }; + } + //******************************** */ + async toggleCategoryStatus(paramDto: ParamDto) { + const category = await this.ticketsCategoryRepository.findCategoryById(paramDto.id); + if (!category) throw new BadRequestException(TicketMessageEnum.CATEGORY_NOT_FOUND); + category.isActive = !category.isActive; + await this.ticketsCategoryRepository.save(category); + return { + message: CommonMessage.UPDATE_SUCCESS, + isActive: category.isActive, + }; + } + + //******************************** */ + + async createTicket() {} +} diff --git a/src/modules/tickets/repositories/tickets-category.repository.ts b/src/modules/tickets/repositories/tickets-category.repository.ts index 50397eb..47ee2f9 100644 --- a/src/modules/tickets/repositories/tickets-category.repository.ts +++ b/src/modules/tickets/repositories/tickets-category.repository.ts @@ -1,11 +1,20 @@ +import { Injectable } from "@nestjs/common"; import { InjectRepository } from "@nestjs/typeorm"; import { Repository } from "typeorm"; + import { TicketCategory } from "../entities/ticket-category.entity"; -import { Injectable } from "@nestjs/common"; @Injectable() export class TicketCategoryRepository extends Repository { constructor(@InjectRepository(TicketCategory) ticketCategoryRepository: Repository) { super(ticketCategoryRepository.target, ticketCategoryRepository.manager, ticketCategoryRepository.queryRunner); } + + async findCategoryByTitle(title: string): Promise { + return this.findOneBy({ title }); + } + + async findCategoryById(id: string): Promise { + return this.findOneBy({ id }); + } } diff --git a/src/modules/tickets/repositories/tickets-message.repository.ts b/src/modules/tickets/repositories/tickets-message.repository.ts index 366d0a1..04ef0c7 100644 --- a/src/modules/tickets/repositories/tickets-message.repository.ts +++ b/src/modules/tickets/repositories/tickets-message.repository.ts @@ -1,7 +1,8 @@ +import { Injectable } from "@nestjs/common"; import { InjectRepository } from "@nestjs/typeorm"; import { Repository } from "typeorm"; + import { TicketMessage } from "../entities/ticket-message.entity"; -import { Injectable } from "@nestjs/common"; @Injectable() export class TicketMessagesRepository extends Repository { diff --git a/src/modules/tickets/repositories/tickets.repository.ts b/src/modules/tickets/repositories/tickets.repository.ts index 52d7c3b..f6e0076 100644 --- a/src/modules/tickets/repositories/tickets.repository.ts +++ b/src/modules/tickets/repositories/tickets.repository.ts @@ -1,7 +1,8 @@ -import { Repository } from "typeorm"; -import { Ticket } from "../entities/ticket.entity"; import { Injectable } from "@nestjs/common"; import { InjectRepository } from "@nestjs/typeorm"; +import { Repository } from "typeorm"; + +import { Ticket } from "../entities/ticket.entity"; @Injectable() export class TicketsRepository extends Repository { diff --git a/src/modules/tickets/tickets.controller.ts b/src/modules/tickets/tickets.controller.ts index 25ffc64..5ddfc80 100644 --- a/src/modules/tickets/tickets.controller.ts +++ b/src/modules/tickets/tickets.controller.ts @@ -1,10 +1,60 @@ -import { Controller } from "@nestjs/common"; -import { ApiTags } from "@nestjs/swagger"; +import { Body, Controller, Get, HttpCode, HttpStatus, Param, Patch, Post, Query } from "@nestjs/common"; +import { ApiOperation, ApiTags } from "@nestjs/swagger"; -// import { TicketsService } from "./providers/tickets.service"; +import { CreateTicketCategoryDto } from "./DTO/create-ticket-category.dto"; +import { SearchTicketCategoryDto } from "./DTO/search-ticket-category.dto"; +import { UpdateTicketCategoryDto } from "./DTO/update-ticket-category.dto"; +import { TicketsService } from "./providers/tickets.service"; +import { AuthGuards } from "../../common/decorators/auth-guard.decorator"; +import { Pagination } from "../../common/decorators/pagination.decorator"; +import { Roles } from "../../common/decorators/roles.decorator"; +import { ParamDto } from "../../common/DTO/param.dto"; +import { RoleEnum } from "../users/enums/role.enum"; @Controller("tickets") @ApiTags("Tickets") export class TicketsController { - // constructor(private readonly ticketsService: TicketsService) {} + constructor(private readonly ticketsService: TicketsService) {} + + @ApiOperation({ summary: "Create ticket category => admin route" }) + @AuthGuards() + @Roles(RoleEnum.ADMIN) + @Post("category") + createTicketCategory(@Body() createDto: CreateTicketCategoryDto) { + return this.ticketsService.createTicketCategory(createDto); + } + + @ApiOperation({ summary: "Update ticket category => admin route" }) + @AuthGuards() + @Roles(RoleEnum.ADMIN) + @Patch("category/:id") + updateCategory(@Param() paramDto: ParamDto, @Body() updateCategoryDto: UpdateTicketCategoryDto) { + return this.ticketsService.updateCategory(paramDto, updateCategoryDto); + } + + @ApiOperation({ summary: "Get ticket categories => admin route" }) + @AuthGuards() + @Roles(RoleEnum.ADMIN) + @Get("categories") + getTicketCategories() { + return this.ticketsService.getTicketCategories(); + } + + @ApiOperation({ summary: "Get ticket categories list => admin route" }) + @Pagination() + @AuthGuards() + @Roles(RoleEnum.ADMIN) + @Get("categories-list") + getCategoriesList(@Query() queryDto: SearchTicketCategoryDto) { + return this.ticketsService.getCategoriesList(queryDto); + } + + @ApiOperation({ summary: "toggle status of categories => admin route" }) + @AuthGuards() + @Roles(RoleEnum.ADMIN) + @HttpCode(HttpStatus.OK) + @Post("category/toggle-status/:id") + toggleCategoryStatus(@Param() paramDto: ParamDto) { + return this.ticketsService.toggleCategoryStatus(paramDto); + } } diff --git a/src/modules/tickets/tickets.module.ts b/src/modules/tickets/tickets.module.ts index b3bcdb4..e8a459a 100644 --- a/src/modules/tickets/tickets.module.ts +++ b/src/modules/tickets/tickets.module.ts @@ -2,16 +2,17 @@ import { Module } from "@nestjs/common"; import { TypeOrmModule } from "@nestjs/typeorm"; import { TicketCategory } from "./entities/ticket-category.entity"; -import { Ticket } from "./entities/ticket.entity"; -import { TicketsController } from "./tickets.controller"; -import { TicketsService } from "./providers/tickets.service"; import { TicketMessage } from "./entities/ticket-message.entity"; -import { TicketsRepository } from "./repositories/tickets.repository"; +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"; @Module({ - imports: [TypeOrmModule.forFeature([Ticket, TicketCategory, TicketMessage])], + imports: [TypeOrmModule.forFeature([Ticket, TicketCategory, TicketMessage]), UsersModule], providers: [TicketsService, TicketsRepository, TicketCategoryRepository, TicketMessagesRepository], controllers: [TicketsController], exports: [TypeOrmModule, TicketsService, TicketsRepository, TicketCategoryRepository, TicketMessagesRepository], diff --git a/src/modules/users/DTO/user-group.dto.ts b/src/modules/users/DTO/user-group.dto.ts index e42cc7b..a30ee3b 100644 --- a/src/modules/users/DTO/user-group.dto.ts +++ b/src/modules/users/DTO/user-group.dto.ts @@ -38,20 +38,12 @@ export class UpdateUserGroupDto { @IsNotEmpty({ message: "نام گروه نمی‌تواند خالی باشد" }) @IsString({ message: "نام گروه باید رشته باشد" }) @MaxLength(100, { message: "نام گروه نمی‌تواند بیش از ۱۰۰ کاراکتر باشد" }) - @ApiProperty({ - example: "گروه کاربران بروزرسانی شده", - description: "نام جدید گروه", - required: false, - }) + @ApiProperty({ example: "گروه کاربران بروزرسانی شده", description: "نام جدید گروه", required: false }) name?: string; @IsOptional() @IsString({ message: "توضیحات باید رشته باشد" }) - @ApiProperty({ - example: "توضیحات جدید برای گروه", - description: "توضیحات اختیاری گروه", - required: false, - }) + @ApiProperty({ example: "توضیحات جدید برای گروه", description: "توضیحات اختیاری گروه", required: false }) description?: string; @IsOptional() diff --git a/src/modules/users/entities/user-group.entity.ts b/src/modules/users/entities/user-group.entity.ts index eb1826d..c0046f0 100644 --- a/src/modules/users/entities/user-group.entity.ts +++ b/src/modules/users/entities/user-group.entity.ts @@ -1,16 +1,17 @@ -import { Column, Entity, JoinTable, ManyToMany } from "typeorm"; +import { Column, Entity, JoinTable, ManyToMany, OneToMany } from "typeorm"; import { User } from "./user.entity"; import { BaseEntity } from "../../../common/entities/base.entity"; import { Announcement } from "../../announcements/entities/announcement.entity"; +import { TicketCategory } from "../../tickets/entities/ticket-category.entity"; @Entity() export class UserGroup extends BaseEntity { - @Column({ type: "varchar", length: 100 }) + @Column({ type: "varchar", length: 100, unique: true }) name: string; @Column({ nullable: true }) - description?: string; + description: string; @ManyToMany(() => User, (user) => user.groups) @JoinTable() @@ -19,4 +20,7 @@ export class UserGroup extends BaseEntity { @ManyToMany(() => Announcement, (announcement) => announcement.targetGroups) @JoinTable() announcements: Announcement[]; + + @OneToMany(() => TicketCategory, (ticketCategory) => ticketCategory.group) + ticketCategories: TicketCategory[]; } diff --git a/src/modules/users/entities/user.entity.ts b/src/modules/users/entities/user.entity.ts index d729541..9855913 100644 --- a/src/modules/users/entities/user.entity.ts +++ b/src/modules/users/entities/user.entity.ts @@ -1,9 +1,11 @@ -import { Column, Entity, ManyToMany, ManyToOne, OneToMany } from "typeorm"; +import { Exclude } from "class-transformer"; +import { Column, Entity, JoinTable, ManyToMany, ManyToOne, OneToMany } from "typeorm"; import { Role } from "./role.entity"; import { UserGroup } from "./user-group.entity"; import { BaseEntity } from "../../../common/entities/base.entity"; import { Criticism } from "../../criticisms/entities/criticism.entity"; +import { DanakService } from "../../danak-services/entities/danak-service.entity"; import { TicketMessage } from "../../tickets/entities/ticket-message.entity"; import { Ticket } from "../../tickets/entities/ticket.entity"; @@ -18,6 +20,7 @@ export class User extends BaseEntity { @Column({ type: "varchar", length: 50, unique: true, nullable: true }) userName: string; + @Exclude() @Column({ type: "varchar", length: 150 }) password: string; @@ -47,4 +50,8 @@ export class User extends BaseEntity { @OneToMany(() => Criticism, (criticism) => criticism.user) criticisms: Criticism[]; + + @ManyToMany(() => DanakService, (danakService) => danakService.users) + @JoinTable() + danakServices: DanakService[]; } diff --git a/src/modules/users/providers/users.service.ts b/src/modules/users/providers/users.service.ts index d4b8c04..e93d2d6 100644 --- a/src/modules/users/providers/users.service.ts +++ b/src/modules/users/providers/users.service.ts @@ -114,38 +114,33 @@ export class UsersService { return { message: CommonMessage.VALID_FOR_CHOOSE }; } - ///****************************************************** */ - // async roleSeeder() { - // const countRole = await this.roleRepository.count(); - // if (countRole > 0) return; - // const createdRoles = await this.roleRepository.insert(roles); - // this.logger.log({ createdRoles }); - // return createdRoles; - // } - async createUserGroup(createUserGroupDto: CreateUserGroupDto) { const users = await this.userRepository.find({ where: { id: In(createUserGroupDto.userIds) } }); const userGroup = this.userGroupRepository.create({ ...createUserGroupDto, users, }); - console.log({ userGroup }); - return await this.userGroupRepository.save(userGroup); + await this.userGroupRepository.save(userGroup); + return { + message: UserMessage.USER_GROUP_CREATED, + }; } + ///****************************************************** */ - async getUserGroup() { - const usersGroup = await this.userGroupRepository.find({}); + async getUserGroups() { + const usersGroup = await this.userGroupRepository.find({ relations: { users: true } }); return { usersGroup, }; } ///****************************************************** */ - // async roleSeeder() { - // const countRole = await this.roleRepository.count(); - // if (countRole > 0) return; - // const createdRoles = await this.roleRepository.insert(roles); - // this.logger.log({ createdRoles }); - // return createdRoles; - // } + + async getUserGroupById(id: string) { + const userGroup = await this.userGroupRepository.findGroupById(id); + if (!userGroup) throw new BadRequestException(UserMessage.USER_GROUP_NOT_FOUND); + return { + userGroup, + }; + } } diff --git a/src/modules/users/repositories/user-group.repository.ts b/src/modules/users/repositories/user-group.repository.ts index 5133b69..d326101 100644 --- a/src/modules/users/repositories/user-group.repository.ts +++ b/src/modules/users/repositories/user-group.repository.ts @@ -9,4 +9,8 @@ export class UserGroupRepository extends Repository { constructor(@InjectRepository(UserGroup) userGroupRepository: Repository) { super(userGroupRepository.target, userGroupRepository.manager, userGroupRepository.queryRunner); } + + async findGroupById(id: string): Promise { + return this.findOneBy({ id }); + } } diff --git a/src/modules/users/users.controller.ts b/src/modules/users/users.controller.ts index a9fe12f..e46c896 100644 --- a/src/modules/users/users.controller.ts +++ b/src/modules/users/users.controller.ts @@ -48,9 +48,10 @@ export class UsersController { } @ApiOperation({ summary: "get all user group" }) - @HttpCode(HttpStatus.OK) + @AuthGuards() + @Roles(RoleEnum.ADMIN) @Get("user-group") UserGroups() { - return this.usersService.getUserGroup(); + return this.usersService.getUserGroups(); } }