chore: add criritisims module

This commit is contained in:
Mahyargdz
2025-05-18 15:02:26 +03:30
parent 4e4abb5a9c
commit 7d0abbeac0
13 changed files with 268 additions and 7 deletions
+2
View File
@@ -18,6 +18,7 @@ import { AnnouncementsModule } from "./modules/announcements/announcement.module
import { AuthModule } from "./modules/auth/auth.module";
import { BusinessesModule } from "./modules/businesses/businesses.module";
import { CompaniesModule } from "./modules/companies/companies.module";
import { CriticismModule } from "./modules/criticisms/criticisms.module";
import { IndustriesModule } from "./modules/industries/industries.module";
import { InvoicesModule } from "./modules/invoices/invoices.module";
import { NotificationModule } from "./modules/notifications/notifications.module";
@@ -55,6 +56,7 @@ import { UtilsModule } from "./modules/utils/utils.module";
InvoicesModule,
BusinessesModule,
AnnouncementsModule,
CriticismModule,
],
// providers: [
// {
@@ -3,6 +3,7 @@ import { Cascade, Collection, Entity, EntityRepositoryType, OneToMany, Property,
import { BaseEntity } from "../../../common/entities/base.entity";
import { Announcement } from "../../announcements/entities/announcement.entity";
import { Company } from "../../companies/entities/company.entity";
import { Criticism } from "../../criticisms/entities/criticism.entity";
import { Industry } from "../../industries/entities/industry.entity";
import { Invoice } from "../../invoices/entities/invoice.entity";
import { TicketCategory } from "../../tickets/entities/ticket-category.entity";
@@ -39,5 +40,8 @@ export class Business extends BaseEntity {
@OneToMany(() => Announcement, (announcement) => announcement.business)
announcements = new Collection<Announcement>(this);
@OneToMany(() => Criticism, (criticism) => criticism.business)
criticisms = new Collection<Criticism>(this);
[EntityRepositoryType]?: BusinessRepository;
}
+25
View File
@@ -0,0 +1,25 @@
import { ApiProperty } from "@nestjs/swagger";
import { IsNotEmpty, IsOptional, IsString, IsUrl } from "class-validator";
import { CriticismMessage } from "../../../common/enums/message.enum";
export class CreateCriticismDto {
@IsNotEmpty({ message: CriticismMessage.TITLE_IS_REQUIRED })
@IsString({ message: CriticismMessage.TITLE_STRING })
@ApiProperty({ type: String, description: "title of criticism", example: "انتقاد بابت مشکلات" })
title: string;
@IsNotEmpty({ message: CriticismMessage.CONTENT_IS_REQUIRED })
@IsString({ message: CriticismMessage.CONTENT_IS_STRING })
@ApiProperty({ type: String, description: "description of criticism", example: "متن انتقاد" })
content: string;
@IsOptional()
@IsNotEmpty({ message: CriticismMessage.FILES_IS_REQUIRED })
@IsUrl({ protocols: ["http", "https"], require_protocol: true }, { message: CriticismMessage.FILES_SHOULD_BE_URL, each: true })
@ApiProperty({
description: "The ulr of the criticism files",
example: ["https://example.com/image1.png", "https://example.com/image2.png"],
})
files: string[];
}
+17
View File
@@ -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 SearchCriticismQueryDto 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;
}
+43
View File
@@ -0,0 +1,43 @@
import { Body, Controller, Delete, Get, Param, Post, Query, UseInterceptors } from "@nestjs/common";
import { ApiOperation } from "@nestjs/swagger";
import { CreateCriticismDto } from "./DTO/create-criticism.dto";
import { SearchCriticismQueryDto } from "./DTO/search-criticism-query.dto";
import { CriticismsService } from "./providers/criticisms.service";
import { AuthGuards } from "../../common/decorators/auth-guard.decorator";
import { BusinessDec } from "../../common/decorators/business.decorator";
import { UserDec } from "../../common/decorators/user.decorator";
import { ParamDto } from "../../common/DTO/param.dto";
import { BusinessInterceptor } from "../../core/interceptors/business.interceptor";
import { Business } from "../businesses/entities/business.entity";
@Controller("criticisms")
@UseInterceptors(BusinessInterceptor)
@AuthGuards()
export class CriticismController {
constructor(private readonly criticismService: CriticismsService) {}
@ApiOperation({ summary: "Create a new criticism" })
@Post()
create(@Body() createDto: CreateCriticismDto, @UserDec("id") userId: string, @BusinessDec() business: Business) {
return this.criticismService.create(createDto, userId, business);
}
@ApiOperation({ summary: "Get all Criticism (admin)" })
@Get()
findAll(@Query() queryDto: SearchCriticismQueryDto, @BusinessDec("id") businessId: string) {
return this.criticismService.findAll(queryDto, businessId);
}
@ApiOperation({ summary: "Get one Criticism with id (admin)" })
@Get(":id")
fineOne(@Param() paramDto: ParamDto, @BusinessDec("id") businessId: string) {
return this.criticismService.findOneAndMarkAsRead(paramDto.id, businessId);
}
@ApiOperation({ summary: "Delete one Criticism with id (admin)" })
@Delete(":id")
delete(@Param() paramDto: ParamDto, @BusinessDec("id") businessId: string) {
return this.criticismService.delete(paramDto.id, businessId);
}
}
+17
View File
@@ -0,0 +1,17 @@
import { MikroOrmModule } from "@mikro-orm/nestjs";
import { Module } from "@nestjs/common";
import { CriticismController } from "./criticisms.controller";
import { CriticismFile } from "./entities/criticism-file.entity";
import { Criticism } from "./entities/criticism.entity";
import { CriticismsService } from "./providers/criticisms.service";
import { BusinessesModule } from "../businesses/businesses.module";
import { UsersModule } from "../users/users.module";
@Module({
imports: [MikroOrmModule.forFeature([Criticism, CriticismFile]), UsersModule, BusinessesModule],
providers: [CriticismsService],
controllers: [CriticismController],
exports: [CriticismsService],
})
export class CriticismModule {}
+16
View File
@@ -0,0 +1,16 @@
import { Entity, EntityRepositoryType, ManyToOne, Property } from "@mikro-orm/postgresql";
import { Criticism } from "./criticism.entity";
import { BaseEntity } from "../../../common/entities/base.entity";
import { CriticismFileRepository } from "../repositories/criticisms-file.repository";
@Entity({ repository: () => CriticismFileRepository })
export class CriticismFile extends BaseEntity {
@Property({ type: "varchar", length: 255 })
fileUrl!: string;
@ManyToOne(() => Criticism, { nullable: false, deleteRule: "cascade" })
criticism!: Criticism;
[EntityRepositoryType]?: CriticismFileRepository;
}
+34
View File
@@ -0,0 +1,34 @@
import { Cascade, Collection, Entity, EntityRepositoryType, Index, ManyToOne, OneToMany, Opt, Property } from "@mikro-orm/core";
import { CriticismFile } from "./criticism-file.entity";
import { BaseEntity } from "../../../common/entities/base.entity";
import { Business } from "../../businesses/entities/business.entity";
import { User } from "../../users/entities/user.entity";
import { CriticismRepository } from "../repositories/criticisms.repository";
@Entity({ repository: () => CriticismRepository })
export class Criticism extends BaseEntity {
@Property({ type: "int", autoincrement: true })
numericId!: number & Opt;
@Index()
@Property({ type: "varchar", length: 150, nullable: false })
title!: string;
@Property({ type: "text", nullable: false })
content!: string;
@ManyToOne(() => User, { nullable: false, deleteRule: "cascade" })
user!: User;
@Property({ type: "boolean", default: false })
isRead!: boolean & Opt;
@OneToMany(() => CriticismFile, (criticismFile) => criticismFile.criticism, { eager: true, cascade: [Cascade.PERSIST, Cascade.REMOVE] })
files = new Collection<CriticismFile>(this);
@ManyToOne(() => Business, { nullable: false, deleteRule: "cascade" })
business!: Business;
[EntityRepositoryType]?: CriticismRepository;
}
+99
View File
@@ -0,0 +1,99 @@
import { EntityManager, QueryOrder } from "@mikro-orm/postgresql";
import { BadRequestException, Injectable } from "@nestjs/common";
import { CommonMessage, CriticismMessage } from "../../../common/enums/message.enum";
import { Business } from "../../businesses/entities/business.entity";
import { UsersService } from "../../users/services/users.service";
import { PaginationUtils } from "../../utils/providers/pagination.utils";
import { CreateCriticismDto } from "../DTO/create-criticism.dto";
import { SearchCriticismQueryDto } from "../DTO/search-criticism-query.dto";
import { Criticism } from "../entities/criticism.entity";
import { CriticismRepository } from "../repositories/criticisms.repository";
@Injectable()
export class CriticismsService {
constructor(
private readonly criticismRepository: CriticismRepository,
private readonly usersService: UsersService,
private readonly em: EntityManager,
) {}
async create(createDto: CreateCriticismDto, userId: string, business: Business) {
const em = this.em.fork();
try {
em.begin();
const user = await this.usersService.findOneByIdWithEntityManager(userId, em);
const files =
createDto.files?.map((file) => {
return { fileUrl: file };
}) || [];
const criticism = em.create(Criticism, { ...createDto, user, files, business });
await em.persistAndFlush(criticism);
await em.commit();
return {
message: CommonMessage.CREATED,
criticism,
};
} catch (error) {
await em.rollback();
throw error;
}
}
//##################################### ##
async findAll(queryDto: SearchCriticismQueryDto, businessId: string) {
const { limit, skip } = PaginationUtils(queryDto);
const queryBuilder = this.criticismRepository.createQueryBuilder("criticism");
queryBuilder.where({ business: { id: businessId } });
if (queryDto.since) {
queryBuilder.andWhere({ createdAt: { $gte: queryDto.since } });
}
if (queryDto.q) {
queryBuilder.andWhere({ title: { $ilike: `%${queryDto.q}%` } });
queryBuilder.andWhere({ content: { $ilike: `%${queryDto.q}%` } });
}
queryBuilder.orderBy({ createdAt: QueryOrder.DESC });
const [criticisms, count] = await queryBuilder.offset(skip).limit(limit).getResultAndCount();
return {
criticisms,
count,
paginate: true,
};
}
//##################################### ##
async findOneAndMarkAsRead(id: string, businessId: string) {
const criticism = await this.criticismRepository.findOne({ id, business: { id: businessId } });
if (!criticism) throw new BadRequestException(CriticismMessage.NOT_FOUND);
criticism.isRead = true;
await this.em.flush();
return {
criticism,
};
}
//##################################### ##
async delete(id: string, businessId: string) {
const criticism = await this.criticismRepository.findOne({ id, business: { id: businessId } });
if (!criticism) throw new BadRequestException(CriticismMessage.NOT_FOUND);
await this.em.removeAndFlush(criticism);
return {
message: CommonMessage.DELETED,
};
}
}
@@ -0,0 +1,5 @@
import { EntityRepository } from "@mikro-orm/postgresql";
import { CriticismFile } from "../entities/criticism-file.entity";
export class CriticismFileRepository extends EntityRepository<CriticismFile> {}
@@ -0,0 +1,5 @@
import { EntityRepository } from "@mikro-orm/postgresql";
import { Criticism } from "../entities/criticism.entity";
export class CriticismRepository extends EntityRepository<Criticism> {}
@@ -1,4 +1,4 @@
import { DecimalType, Entity, EntityRepositoryType, ManyToOne, Property } from "@mikro-orm/postgresql";
import { DecimalType, Entity, EntityRepositoryType, ManyToOne, Property } from "@mikro-orm/core";
import Decimal from "decimal.js";
import { Invoice } from "./invoice.entity";
@@ -9,7 +9,6 @@ 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 { BusinessDec } from "../../common/decorators/business.decorator";
import { UserDec } from "../../common/decorators/user.decorator";
@@ -51,14 +50,12 @@ export class TicketsController {
}
@ApiOperation({ summary: "Get ticket categories list (admin)" })
@AdminRoute()
@Get("categories-list")
getCategoriesList(@Query() queryDto: SearchTicketCategoryDto, @BusinessDec("id") businessId: string) {
return this.ticketsService.getCategoriesList(queryDto, businessId);
}
@ApiOperation({ summary: "toggle status of categories (admin)" })
@AdminRoute()
@HttpCode(HttpStatus.OK)
@Post("category/toggle-status/:id")
toggleCategoryStatus(@Param() paramDto: ParamDto, @BusinessDec("id") businessId: string) {
@@ -80,7 +77,6 @@ export class TicketsController {
}
@ApiOperation({ summary: "Get all tickets (admin)" })
@AdminRoute()
@Get("admin")
getAllTickets(@Query() queryDto: SearchTicketQueryDto, @UserDec("id") adminId: string, @BusinessDec("id") businessId: string) {
return this.ticketsService.getTicketForAdmin(queryDto, adminId, businessId);
@@ -122,14 +118,12 @@ export class TicketsController {
}
@ApiOperation({ summary: "open ticket by admin" })
@AdminRoute()
@Post(":id/open")
openTicketByAdmin(@Param() paramDto: ParamDto, @BusinessDec("id") businessId: string) {
return this.ticketsService.openTicketByAdmin(paramDto.id, businessId);
}
@ApiOperation({ summary: "referer ticket ==> admin route" })
@AdminRoute()
@Post(":id/refer")
referTicket(
@Param() paramDto: ParamDto,