crtisim
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -21,6 +21,7 @@ import { RequestModule } from './modules/request/request.module';
|
||||
import { InvoiceModule } from './modules/invoice/invoice.module';
|
||||
import { OrderModule } from './modules/order/order.module';
|
||||
import { LearningModule } from './modules/learnings/learning.module';
|
||||
import { CriticismModule } from './modules/criticisms/criticisms.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -50,6 +51,7 @@ import { LearningModule } from './modules/learnings/learning.module';
|
||||
InvoiceModule,
|
||||
OrderModule,
|
||||
LearningModule,
|
||||
CriticismModule,
|
||||
],
|
||||
controllers: [],
|
||||
providers: [],
|
||||
|
||||
@@ -137,6 +137,17 @@ export const enum LearningMessage {
|
||||
VIDEO_DURATION_INVALID = 'مدت زمان ویدیو نامعتبر است',
|
||||
}
|
||||
|
||||
export const enum CriticismMessage {
|
||||
NOT_FOUND = 'انتقاد مورد نظر یافت نشد',
|
||||
TITLE_IS_REQUIRED = 'عنوان الزامی است',
|
||||
TITLE_STRING = 'عنوان باید رشته باشد',
|
||||
CONTENT_IS_REQUIRED = 'محتوا الزامی است',
|
||||
CONTENT_IS_STRING = 'محتوا باید رشته باشد',
|
||||
FILES_IS_REQUIRED = 'فایلها الزامی است',
|
||||
FILES_SHOULD_BE_URL = 'آدرس فایل باید URL معتبر باشد',
|
||||
SEARCH_QUERY_STRING = 'رشته جستجو باید یک رشته باشد',
|
||||
}
|
||||
|
||||
export const enum productMessage {
|
||||
NOT_FOUND = 'کالایی با این مشخصات یافت نشد',
|
||||
}
|
||||
|
||||
@@ -43,6 +43,8 @@ export enum PermissionEnum {
|
||||
MANAGE_PRINT = 'view_print',
|
||||
|
||||
MANAGE_LEARNINGS = 'manage_learnings',
|
||||
|
||||
MANAGE_CRITICISMS = 'manage_criticisms',
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -77,5 +79,6 @@ export const PermissionTitles: Record<PermissionEnum, string> = {
|
||||
[PermissionEnum.DELETE_ORDER]: "",
|
||||
[PermissionEnum.CREATE_PRINT]: "ایجاد فرم چاپ برای سفارش",
|
||||
[PermissionEnum.MANAGE_PRINT]: "مدیریت فرم چاپ",
|
||||
[PermissionEnum.MANAGE_LEARNINGS]: "مدیریت آموزشها"
|
||||
[PermissionEnum.MANAGE_LEARNINGS]: "مدیریت آموزشها",
|
||||
[PermissionEnum.MANAGE_CRITICISMS]: "مدیریت انتقادها و پیشنهادها"
|
||||
};
|
||||
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
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()
|
||||
@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[];
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
import { Body, Controller, Delete, Get, Param, Post, Query, UseGuards } from '@nestjs/common';
|
||||
import { ApiBearerAuth, ApiOperation, ApiTags } 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 { AuthGuard } from '../auth/guards/auth.guard';
|
||||
import { AdminAuthGuard } from '../auth/guards/adminAuth.guard';
|
||||
import { Permissions } from '../../common/decorators/permissions.decorator';
|
||||
import { UserId } from '../../common/decorators';
|
||||
import { PermissionEnum } from '../../common/enums/permission.enum';
|
||||
|
||||
@ApiTags('Criticism')
|
||||
@ApiBearerAuth()
|
||||
@Controller('criticisms')
|
||||
export class CriticismController {
|
||||
constructor(private readonly criticismService: CriticismsService) {}
|
||||
|
||||
@Post()
|
||||
@UseGuards(AuthGuard)
|
||||
@ApiOperation({ summary: 'Create a new criticism' })
|
||||
create(@Body() createDto: CreateCriticismDto, @UserId() userId: string) {
|
||||
return this.criticismService.create(createDto, userId);
|
||||
}
|
||||
|
||||
@Get()
|
||||
@UseGuards(AdminAuthGuard)
|
||||
@Permissions(PermissionEnum.MANAGE_CRITICISMS)
|
||||
@ApiOperation({ summary: 'Get all criticisms (admin only)' })
|
||||
findAll(@Query() queryDto: SearchCriticismQueryDto) {
|
||||
return this.criticismService.findAll(queryDto);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@UseGuards(AdminAuthGuard)
|
||||
@Permissions(PermissionEnum.MANAGE_CRITICISMS)
|
||||
@ApiOperation({ summary: 'Get one criticism by id (admin only)' })
|
||||
findOne(@Param('id') id: string) {
|
||||
return this.criticismService.findOneAndMarkAsRead(id);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@UseGuards(AdminAuthGuard)
|
||||
@Permissions(PermissionEnum.MANAGE_CRITICISMS)
|
||||
@ApiOperation({ summary: 'Delete one criticism by id (admin only)' })
|
||||
delete(@Param('id') id: string) {
|
||||
return this.criticismService.delete(id);
|
||||
}
|
||||
}
|
||||
Executable
+27
@@ -0,0 +1,27 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { MikroOrmModule } from '@mikro-orm/nestjs';
|
||||
|
||||
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 { CriticismFileRepository } from './repositories/criticisms-file.repository';
|
||||
import { CriticismRepository } from './repositories/criticisms.repository';
|
||||
import { UserModule } from '../user/user.module';
|
||||
import { NotificationsModule } from '../notification/notifications.module';
|
||||
import { AdminModule } from '../admin/admin.module';
|
||||
import { JwtModule } from '@nestjs/jwt';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
MikroOrmModule.forFeature([Criticism, CriticismFile]),
|
||||
UserModule,
|
||||
NotificationsModule,
|
||||
AdminModule,
|
||||
JwtModule
|
||||
],
|
||||
providers: [CriticismsService, CriticismRepository, CriticismFileRepository],
|
||||
controllers: [CriticismController],
|
||||
exports: [CriticismsService],
|
||||
})
|
||||
export class CriticismModule {}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Entity, ManyToOne, OptionalProps, Property } from '@mikro-orm/core';
|
||||
import { BaseEntity } from '../../../common/entities/base.entity';
|
||||
import { Criticism } from './criticism.entity';
|
||||
|
||||
@Entity({ tableName: 'criticism_files' })
|
||||
export class CriticismFile extends BaseEntity {
|
||||
[OptionalProps]?: 'createdAt' | 'deletedAt';
|
||||
|
||||
@Property({ type: 'varchar', length: 255 })
|
||||
fileUrl: string;
|
||||
|
||||
@ManyToOne(() => Criticism)
|
||||
criticism: Criticism;
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
import { Cascade, Collection, Entity, ManyToOne, OneToMany, OptionalProps, Property } from '@mikro-orm/core';
|
||||
import { BaseEntity } from '../../../common/entities/base.entity';
|
||||
import { User } from '../../user/entities/user.entity';
|
||||
import { CriticismFile } from './criticism-file.entity';
|
||||
|
||||
@Entity({ tableName: 'criticisms' })
|
||||
export class Criticism extends BaseEntity {
|
||||
[OptionalProps]?: 'createdAt' | 'deletedAt';
|
||||
|
||||
@Property({ type: 'varchar', length: 150 })
|
||||
title: string;
|
||||
|
||||
@Property({ type: 'text' })
|
||||
content: string;
|
||||
|
||||
@ManyToOne(() => User)
|
||||
user: User;
|
||||
|
||||
@Property({ default: false })
|
||||
isRead: boolean = false;
|
||||
|
||||
@OneToMany(() => CriticismFile, (criticismFile) => criticismFile.criticism, { cascade: [Cascade.ALL] })
|
||||
files = new Collection<CriticismFile>(this);
|
||||
}
|
||||
+135
@@ -0,0 +1,135 @@
|
||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
import { EntityManager } from '@mikro-orm/postgresql';
|
||||
import { CommonMessage, CriticismMessage } from '../../../common/enums/message.enum';
|
||||
import { NotificationQueueService } from '../../notification/services/notification-queue.service';
|
||||
import { AdminRepository } from '../../admin/repositories/admin.repository';
|
||||
import { PermissionEnum } from '../../../common/enums/permission.enum';
|
||||
import { NotifEvent } from '../../notification/interfaces/notification.interface';
|
||||
import { Notification } from '../../notification/entities/notification.entity';
|
||||
import { CreateCriticismDto } from '../DTO/create-criticism.dto';
|
||||
import { SearchCriticismQueryDto } from '../DTO/search-criticism-query.dto';
|
||||
import { Criticism } from '../entities/criticism.entity';
|
||||
import { CriticismFile } from '../entities/criticism-file.entity';
|
||||
import { CriticismRepository } from '../repositories/criticisms.repository';
|
||||
import { UserService } from '../../user/providers/user.service';
|
||||
|
||||
@Injectable()
|
||||
export class CriticismsService {
|
||||
constructor(
|
||||
private readonly criticismRepository: CriticismRepository,
|
||||
private readonly userService: UserService,
|
||||
private readonly em: EntityManager,
|
||||
private readonly adminRepository: AdminRepository,
|
||||
private readonly notificationQueueService: NotificationQueueService,
|
||||
) {}
|
||||
|
||||
async create(createDto: CreateCriticismDto, userId: string) {
|
||||
return this.em.transactional(async (em) => {
|
||||
const user = await this.userService.findOrFail(userId);
|
||||
|
||||
const criticism = em.create(Criticism, {
|
||||
title: createDto.title,
|
||||
content: createDto.content,
|
||||
user,
|
||||
isRead: false,
|
||||
});
|
||||
|
||||
for (const fileUrl of createDto.files ?? []) {
|
||||
const file = em.create(CriticismFile, { fileUrl, criticism });
|
||||
criticism.files.add(file);
|
||||
}
|
||||
|
||||
await em.persistAndFlush(criticism);
|
||||
|
||||
await this.notifyAdminsWithPermission(criticism, em);
|
||||
|
||||
return {
|
||||
message: CommonMessage.CREATED,
|
||||
criticism,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async findAll(queryDto: SearchCriticismQueryDto) {
|
||||
const page = queryDto.page ?? 1;
|
||||
const limit = queryDto.limit ?? 10;
|
||||
const offset = (page - 1) * limit;
|
||||
|
||||
const where: Record<string, unknown> = {};
|
||||
|
||||
if (queryDto.since) {
|
||||
where.createdAt = { $gte: new Date(queryDto.since) };
|
||||
}
|
||||
|
||||
if (queryDto.q) {
|
||||
where.title = { $like: `%${queryDto.q}%` };
|
||||
}
|
||||
|
||||
const [criticisms, count] = await this.em.findAndCount(Criticism, where, {
|
||||
orderBy: { createdAt: 'DESC' },
|
||||
offset,
|
||||
limit,
|
||||
populate: ['user', 'files'],
|
||||
});
|
||||
|
||||
return {
|
||||
criticisms,
|
||||
count,
|
||||
paginate: true,
|
||||
};
|
||||
}
|
||||
|
||||
async findOneAndMarkAsRead(id: string) {
|
||||
const criticism = await this.criticismRepository.findOne(
|
||||
{ id },
|
||||
{ populate: ['user', 'files'] },
|
||||
);
|
||||
if (!criticism) throw new BadRequestException(CriticismMessage.NOT_FOUND);
|
||||
criticism.isRead = true;
|
||||
await this.em.flush();
|
||||
return {
|
||||
criticism,
|
||||
};
|
||||
}
|
||||
|
||||
async delete(id: string) {
|
||||
const criticism = await this.criticismRepository.findOne({ id });
|
||||
if (!criticism) throw new BadRequestException(CriticismMessage.NOT_FOUND);
|
||||
|
||||
await this.em.removeAndFlush(criticism);
|
||||
return {
|
||||
message: CommonMessage.DELETED,
|
||||
};
|
||||
}
|
||||
|
||||
private async notifyAdminsWithPermission(criticism: Criticism, em: EntityManager) {
|
||||
const admins = await this.adminRepository.findAdminsWithPermission(
|
||||
PermissionEnum.MANAGE_CRITICISMS,
|
||||
);
|
||||
|
||||
if (admins.length === 0) return;
|
||||
|
||||
const user = criticism.user;
|
||||
const fullName = `${user.firstName ?? ''} ${user.lastName ?? ''}`.trim() || 'کاربر';
|
||||
const body = `انتقاد جدید از ${fullName}: ${criticism.title}`;
|
||||
|
||||
const notifications = admins.map((admin) =>
|
||||
em.create(Notification, {
|
||||
admin,
|
||||
title: NotifEvent.NEW_CRITICISM,
|
||||
content: body,
|
||||
}),
|
||||
);
|
||||
|
||||
await em.persistAndFlush(notifications);
|
||||
|
||||
await this.notificationQueueService.addBulkInAppNotifications(
|
||||
notifications.map((n) => ({
|
||||
recipient: { adminId: n.admin!.id },
|
||||
subject: NotifEvent.NEW_CRITICISM,
|
||||
body,
|
||||
notificationId: n.id,
|
||||
})),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { EntityManager, EntityRepository } from '@mikro-orm/postgresql';
|
||||
import { CriticismFile } from '../entities/criticism-file.entity';
|
||||
|
||||
@Injectable()
|
||||
export class CriticismFileRepository extends EntityRepository<CriticismFile> {
|
||||
constructor(readonly em: EntityManager) {
|
||||
super(em, CriticismFile);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { EntityManager, EntityRepository } from '@mikro-orm/postgresql';
|
||||
import { Criticism } from '../entities/criticism.entity';
|
||||
|
||||
@Injectable()
|
||||
export class CriticismRepository extends EntityRepository<Criticism> {
|
||||
constructor(readonly em: EntityManager) {
|
||||
super(em, Criticism);
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ export enum NotifEvent {
|
||||
NEW_ORDER = 'newOrder', // for chapkhane sms bere
|
||||
DESIGNER_ASSIGN = 'designerAssign',
|
||||
PAYMENT_SUCCESS = 'paymentSuccess',
|
||||
NEW_CRITICISM = 'newCriticism',
|
||||
}
|
||||
export type recipientType = { userId: string } | { adminId: string };
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ import { UserModule } from '../user/user.module';
|
||||
import { UtilsModule } from '../util/utils.module';
|
||||
import { ProductModule } from '../product/product.module';
|
||||
import { TicketModule } from '../ticket/ticket.module';
|
||||
import { OrderPrintRepository } from './repositories/order-print.repository';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -32,7 +33,7 @@ import { TicketModule } from '../ticket/ticket.module';
|
||||
],
|
||||
controllers: [OrderController],
|
||||
providers: [OrderService, OrderRepository, OrderListeners,
|
||||
OrdersCrone,],
|
||||
OrdersCrone, OrderPrintRepository],
|
||||
exports: [OrderRepository, OrderService],
|
||||
})
|
||||
export class OrderModule { }
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Entity, Index, Property, OneToMany, Collection, Cascade, PrimaryKey, OptionalProps } from '@mikro-orm/core';
|
||||
import { Entity, Index, Property, OneToMany, Collection, PrimaryKey, OptionalProps } from '@mikro-orm/core';
|
||||
import { BaseEntity } from '../../../common/entities/base.entity';
|
||||
import { LearningProgress } from '../../learnings/entities/learning-progress.entity';
|
||||
import { Criticism } from '../../criticisms/entities/criticism.entity';
|
||||
// import { Order } from 'src/modules/order/entities/order.entity';
|
||||
import { normalizePhone } from '../../util/phone.util';
|
||||
import { ulid } from 'ulid';
|
||||
@@ -12,6 +13,9 @@ export class User extends BaseEntity {
|
||||
@OneToMany(() => LearningProgress, (lp) => lp.user)
|
||||
learningProgress = new Collection<LearningProgress>(this);
|
||||
|
||||
@OneToMany(() => Criticism, (c) => c.user)
|
||||
criticisms = new Collection<Criticism>(this);
|
||||
|
||||
// @OneToMany(() => Order, order => order.user)
|
||||
// orders = new Collection<Order>(this);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user