From c12e0fb4d464f1879c1958c85cc50fd432880e12 Mon Sep 17 00:00:00 2001 From: Matin Date: Tue, 4 Feb 2025 11:41:15 +0330 Subject: [PATCH] chore: contact us module --- src/app.module.ts | 2 + src/common/enums/message.enum.ts | 12 +++ src/modules/auth/auth.controller.ts | 2 +- .../contact-us/DTO/create-contact-us.dto.ts | 16 ++++ .../DTO/search-contact-us-query.dto.ts | 17 ++++ .../contact-us/contact-us.controller.ts | 45 ++++++++++ src/modules/contact-us/contact-us.module.ts | 16 ++++ .../contact-us/entities/contact-us.entity.ts | 22 +++++ .../providers/contact-us.service.ts | 89 +++++++++++++++++++ .../repositories/contact-us.repository.ts | 35 ++++++++ src/modules/users/entities/user.entity.ts | 4 + 11 files changed, 259 insertions(+), 1 deletion(-) create mode 100644 src/modules/contact-us/DTO/create-contact-us.dto.ts create mode 100644 src/modules/contact-us/DTO/search-contact-us-query.dto.ts create mode 100644 src/modules/contact-us/contact-us.controller.ts create mode 100644 src/modules/contact-us/contact-us.module.ts create mode 100644 src/modules/contact-us/entities/contact-us.entity.ts create mode 100644 src/modules/contact-us/providers/contact-us.service.ts create mode 100644 src/modules/contact-us/repositories/contact-us.repository.ts diff --git a/src/app.module.ts b/src/app.module.ts index 14e28b9..f343e3a 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -11,6 +11,7 @@ import { databaseConfigs } from "./configs/typeorm.config"; import { HTTPLogger } from "./core/middlewares/logger.middleware"; import { AnnouncementsModule } from "./modules/announcements/announcement.module"; import { AuthModule } from "./modules/auth/auth.module"; +import { ContactUsModule } from "./modules/contact-us/contact-us.module"; import { CriticismModule } from "./modules/criticisms/criticisms.module"; import { DanakServicesModule } from "./modules/danak-services/danak-services.module"; import { SettingModule } from "./modules/settings/settings.module"; @@ -33,6 +34,7 @@ import { UsersModule } from "./modules/users/users.module"; UploaderModule, CriticismModule, SettingModule, + ContactUsModule, ], controllers: [], providers: [], diff --git a/src/common/enums/message.enum.ts b/src/common/enums/message.enum.ts index 3f6609e..6733435 100644 --- a/src/common/enums/message.enum.ts +++ b/src/common/enums/message.enum.ts @@ -187,3 +187,15 @@ export const enum SettingMessageEnum { SMS_MUST_BE_BOOLEAN = "تنظیمات اسمس باید از نوع BOOLEAN باشد", EMAIL_MUST_BE_BOOLEAN = "تنظیمات ایمیل باید از نوع BOOLEAN باشد", } + +export const enum ContactUsMessage { + FULLNAME_IS_REQUIRED = "نام کامل مورد نیاز است", + FULLNAME_STRING = "نام کامل باید یک رشته باشد", + EMAIL_IS_REQUIRED = "ایمیل مورد نیاز است", + INVALID_EMAIL_FORMAT = "فرمت ایمیل وارد شده صحیح نیست", + CONTENT_IS_REQUIRED = "محتوا مورد نیاز است", + CONTENT_STRING = "محتوا باید یک رشته باشد", + TITLE_IS_REQUIRED = "عنوان مورد نیاز است", + TITLE_STRING = "عنوان باید یک رشته باشد", + NOT_FOUND = "پیامی با این ایدی یافت نشد", +} diff --git a/src/modules/auth/auth.controller.ts b/src/modules/auth/auth.controller.ts index d8b9d20..b8767af 100644 --- a/src/modules/auth/auth.controller.ts +++ b/src/modules/auth/auth.controller.ts @@ -79,7 +79,7 @@ export class AuthController { @AuthGuards() @ApiOperation({ summary: "Update user password" }) @HttpCode(HttpStatus.OK) - @Patch("password") + @Patch("change-password") updatePassword(@UserDec() user: User, @Body() updatePasswordDto: UpdatePasswordDto) { return this.authService.updatePassword(user.id, updatePasswordDto); } diff --git a/src/modules/contact-us/DTO/create-contact-us.dto.ts b/src/modules/contact-us/DTO/create-contact-us.dto.ts new file mode 100644 index 0000000..ed93adb --- /dev/null +++ b/src/modules/contact-us/DTO/create-contact-us.dto.ts @@ -0,0 +1,16 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { IsNotEmpty, IsString } from "class-validator"; + +import { ContactUsMessage } from "../../../common/enums/message.enum"; + +export class CreateContactUsDto { + @IsNotEmpty({ message: ContactUsMessage.TITLE_IS_REQUIRED }) + @IsString({ message: ContactUsMessage.TITLE_STRING }) + @ApiProperty({ description: "title of contact us", example: "عنوان پیام" }) + title: string; + + @IsNotEmpty({ message: ContactUsMessage.CONTENT_IS_REQUIRED }) + @IsString({ message: ContactUsMessage.CONTENT_STRING }) + @ApiProperty({ description: "content of contact us", example: "متن پیام" }) + content: string; +} diff --git a/src/modules/contact-us/DTO/search-contact-us-query.dto.ts b/src/modules/contact-us/DTO/search-contact-us-query.dto.ts new file mode 100644 index 0000000..2b58c36 --- /dev/null +++ b/src/modules/contact-us/DTO/search-contact-us-query.dto.ts @@ -0,0 +1,17 @@ +import { ApiPropertyOptional } from "@nestjs/swagger"; +import { IsDateString, IsOptional, IsString } from "class-validator"; + +import { PaginationDto } from "../../../common/DTO/pagination.dto"; +import { CriticismMessage } from "../../../common/enums/message.enum"; + +export class SearchContactUsQueryDto extends PaginationDto { + @IsOptional() + @IsDateString() + @ApiPropertyOptional({ description: "since query", example: "2025-01-27" }) + since?: string; + + @IsOptional() + @IsString({ message: CriticismMessage.SEARCH_QUERY_STRING }) + @ApiPropertyOptional({ description: "Search query", example: "search query" }) + q?: string; +} diff --git a/src/modules/contact-us/contact-us.controller.ts b/src/modules/contact-us/contact-us.controller.ts new file mode 100644 index 0000000..2b1eed9 --- /dev/null +++ b/src/modules/contact-us/contact-us.controller.ts @@ -0,0 +1,45 @@ +import { Body, Controller, Get, Param, Post, Query } from "@nestjs/common"; +import { ApiOperation, ApiTags } from "@nestjs/swagger"; + +import { CreateContactUsDto } from "./DTO/create-contact-us.dto"; +import { SearchContactUsQueryDto } from "./DTO/search-contact-us-query.dto"; +import { ContactUsService } from "./providers/contact-us.service"; +import { AuthGuards } from "../../common/decorators/auth-guard.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("contact-us") +@ApiTags("ContactUs") +export class ContactUsController { + constructor(private readonly contactUsService: ContactUsService) {} + + @AuthGuards() + @ApiOperation({ summary: "Create a new contact us" }) + @Post() + createContactUs(@UserDec() user: User, @Body() createContactUsDto: CreateContactUsDto) { + return this.contactUsService.createContactUs(user.id, createContactUsDto); + } + + //******************** */ + + @AuthGuards() + @Roles(RoleEnum.ADMIN) + @ApiOperation({ summary: "Get all contact us ===> login as admin" }) + @Get() + getAllContactUs(@Query() queryDto: SearchContactUsQueryDto) { + return this.contactUsService.getAllContactUs(queryDto); + } + + //******************** */ + + @AuthGuards() + @Roles(RoleEnum.ADMIN) + @ApiOperation({ summary: "Get one contact us with id ===> login as admin" }) + @Get(":id") + getOneContactUs(@Param() paramDto: ParamDto) { + return this.contactUsService.getOneContactUs(paramDto.id); + } +} diff --git a/src/modules/contact-us/contact-us.module.ts b/src/modules/contact-us/contact-us.module.ts new file mode 100644 index 0000000..6ec0464 --- /dev/null +++ b/src/modules/contact-us/contact-us.module.ts @@ -0,0 +1,16 @@ +import { Module } from "@nestjs/common"; +import { TypeOrmModule } from "@nestjs/typeorm"; + +import { ContactUsController } from "./contact-us.controller"; +import { ContactUs } from "./entities/contact-us.entity"; +import { ContactUsService } from "./providers/contact-us.service"; +import { UsersModule } from "../users/users.module"; +import { ContactUsRepository } from "./repositories/contact-us.repository"; + +@Module({ + imports: [TypeOrmModule.forFeature([ContactUs]), UsersModule], + providers: [ContactUsRepository, ContactUsService], + controllers: [ContactUsController], + exports: [], +}) +export class ContactUsModule {} diff --git a/src/modules/contact-us/entities/contact-us.entity.ts b/src/modules/contact-us/entities/contact-us.entity.ts new file mode 100644 index 0000000..e661ff9 --- /dev/null +++ b/src/modules/contact-us/entities/contact-us.entity.ts @@ -0,0 +1,22 @@ +import { Column, Entity, ManyToOne } from "typeorm"; + +import { BaseEntity } from "../../../common/entities/base.entity"; +import { User } from "../../users/entities/user.entity"; + +@Entity() +export class ContactUs extends BaseEntity { + @Column({ type: "int", generated: "identity", insert: false }) + numericId: number; + + @Column({ type: "varchar", length: 150, nullable: false }) + title: string; + + @Column({ type: "text", nullable: false }) + content: string; + + @ManyToOne(() => User, (user) => user.contactUs, { nullable: false, onDelete: "CASCADE" }) + user: User; + + @Column({ type: "boolean", default: false }) + isRead: boolean; +} diff --git a/src/modules/contact-us/providers/contact-us.service.ts b/src/modules/contact-us/providers/contact-us.service.ts new file mode 100644 index 0000000..9dada7a --- /dev/null +++ b/src/modules/contact-us/providers/contact-us.service.ts @@ -0,0 +1,89 @@ +import { BadRequestException, Injectable } from "@nestjs/common"; + +import { CommonMessage, ContactUsMessage } from "../../../common/enums/message.enum"; +import { UsersService } from "../../users/providers/users.service"; +import { PaginationUtils } from "../../utils/providers/pagination.utils"; +import { CreateContactUsDto } from "../DTO/create-contact-us.dto"; +import { SearchContactUsQueryDto } from "../DTO/search-contact-us-query.dto"; +import { ContactUsRepository } from "../repositories/contact-us.repository"; + +@Injectable() +export class ContactUsService { + constructor( + private readonly contactUsRepository: ContactUsRepository, + private readonly usersService: UsersService, + ) {} + + //****************************** */ + + async createContactUs(userId: string, createContactUsDto: CreateContactUsDto) { + const { user } = await this.usersService.findOneById(userId); + + const contactUs = this.contactUsRepository.create({ + ...createContactUsDto, + user, + }); + await this.contactUsRepository.save(contactUs); + + return { + message: CommonMessage.CREATED, + contactUs, + }; + } + + //****************************** */ + + async getAllContactUs(queryDto: SearchContactUsQueryDto) { + const { limit, skip } = PaginationUtils(queryDto); + + const queryBuilder = this.contactUsRepository + .createQueryBuilder("contactus") + .leftJoinAndSelect("contactus.user", "user") + .select([ + "contactus.id", + "contactus.numericId", + "contactus.title", + "contactus.content", + "contactus.isRead", + "contactus.createdAt", + "user.id", + "user.email", + "user.firstName", + "user.lastName", + ]); + + if (queryDto.since) { + queryBuilder.andWhere("contactus.createdAt >= :since", { since: queryDto.since }); + } + + if (queryDto.q) { + queryBuilder.andWhere( + "(contactus.title LIKE :q OR contactus.content LIKE :q OR user.email LIKE :q OR user.firstName LIKE :q OR user.lastName LIKE :q)", + { + q: `%${queryDto.q}%`, + }, + ); + } + queryBuilder.orderBy("contactus.createdAt", "DESC"); + + const [contactUs, count] = await queryBuilder.skip(skip).take(limit).getManyAndCount(); + + return { + contactUs, + count, + }; + } + + //****************************** */ + + async getOneContactUs(id: string) { + const contactUs = await this.contactUsRepository.getOneContactUs(id); + + if (!contactUs) throw new BadRequestException(ContactUsMessage.NOT_FOUND); + + contactUs.isRead = true; + await this.contactUsRepository.save(contactUs); + + return { contactUs }; + } +} diff --git a/src/modules/contact-us/repositories/contact-us.repository.ts b/src/modules/contact-us/repositories/contact-us.repository.ts new file mode 100644 index 0000000..567f582 --- /dev/null +++ b/src/modules/contact-us/repositories/contact-us.repository.ts @@ -0,0 +1,35 @@ +import { Injectable } from "@nestjs/common"; +import { InjectRepository } from "@nestjs/typeorm"; +import { Repository } from "typeorm"; + +import { ContactUs } from "../entities/contact-us.entity"; + +@Injectable() +export class ContactUsRepository extends Repository { + constructor(@InjectRepository(ContactUs) contactUsRepository: Repository) { + super(contactUsRepository.target, contactUsRepository.manager, contactUsRepository.queryRunner); + } + + async getOneContactUs(id: string) { + return await this.findOne({ + where: { + id, + }, + relations: ["user"], + select: { + id: true, + numericId: true, + title: true, + content: true, + isRead: true, + createdAt: true, + user: { + id: true, + email: true, + firstName: true, + lastName: true, + }, + }, + }); + } +} diff --git a/src/modules/users/entities/user.entity.ts b/src/modules/users/entities/user.entity.ts index 70d9454..460eaeb 100644 --- a/src/modules/users/entities/user.entity.ts +++ b/src/modules/users/entities/user.entity.ts @@ -5,6 +5,7 @@ import { Role } from "./role.entity"; import { UserGroup } from "./user-group.entity"; import { BaseEntity } from "../../../common/entities/base.entity"; import { UserAnnouncement } from "../../announcements/entities/user-announcement.entity"; +import { ContactUs } from "../../contact-us/entities/contact-us.entity"; import { Criticism } from "../../criticisms/entities/criticism.entity"; import { DanakService } from "../../danak-services/entities/danak-service.entity"; import { UserSetting } from "../../settings/entities/user-setting.entity"; @@ -53,6 +54,9 @@ export class User extends BaseEntity { @OneToMany(() => Criticism, (criticism) => criticism.user) criticisms: Criticism[]; + @OneToMany(() => ContactUs, (contactUs) => contactUs.user) + contactUs: ContactUs[]; + @ManyToMany(() => DanakService, (danakService) => danakService.users) @JoinTable() danakServices: DanakService[];