diff --git a/src/app.module.ts b/src/app.module.ts index 4a3752c..19a952e 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -23,6 +23,7 @@ import { IndustriesModule } from "./modules/industries/industries.module"; import { InvoicesModule } from "./modules/invoices/invoices.module"; import { NotificationModule } from "./modules/notifications/notifications.module"; import { PaymentsModule } from "./modules/payments/payments.module"; +import { SlidersModule } from "./modules/sliders/sliders.module"; import { TicketsModule } from "./modules/tickets/tickets.module"; import { UploaderModule } from "./modules/uploader/uploader.module"; import { UsersModule } from "./modules/users/users.module"; @@ -59,6 +60,7 @@ import { UtilsModule } from "./modules/utils/utils.module"; AnnouncementsModule, CriticismModule, PaymentsModule, + SlidersModule, ], }) export class AppModule implements NestModule { diff --git a/src/common/enums/message.enum.ts b/src/common/enums/message.enum.ts index 37300f6..ff483b0 100755 --- a/src/common/enums/message.enum.ts +++ b/src/common/enums/message.enum.ts @@ -650,3 +650,19 @@ export const enum BusinessMessage { DOMAIN_SETUP_TROUBLESHOOTING_5 = "- برخی از ارائه‌دهندگان DNS ممکن است نیاز به تنظیمات خاص برای رکوردهای TXT داشته باشند", DOMAIN_SETUP_TROUBLESHOOTING_6 = "- اگر از Cloudflare استفاده می‌کنید، مطمئن شوید که رکوردهای TXT را بدون پروکسی تنظیم کرده‌اید", } + +export const enum SliderMessage { + TITLE_REQUIRED = "عنوان اسلایدر الزامی است", + TITLE_STRING = "عنوان اسلایدر باید یک رشته باشد", + TITLE_MAX_LENGTH = "عنوان اسلایدر باید حداکثر ۱۰۰ کاراکتر باشد", + DESCRIPTION_STRING = "توضیحات اسلایدر باید یک رشته باشد", + DESCRIPTION_MAX_LENGTH = "توضیحات اسلایدر باید حداکثر ۲۵۵ کاراکتر باشد", + IMAGE_URL_REQUIRED = "آدرس تصویر اسلایدر الزامی است", + IMAGE_URL_STRING = "آدرس تصویر اسلایدر باید یک رشته باشد", + IMAGE_URL_MAX_LENGTH = "آدرس تصویر اسلایدر باید حداکثر ۲۵۵ کاراکتر باشد", + LINK_STRING = "لینک اسلایدر باید یک رشته باشد", + LINK_MAX_LENGTH = "لینک اسلایدر باید حداکثر ۲۵۵ کاراکتر باشد", + ORDER_NUMBER = "ترتیب اسلایدر باید یک عدد باشد", + IS_ACTIVE_SHOULD_BE_A_BOOLEAN = "وضعیت فعال بودن اسلایدر باید یک مقدار بولی باشد", + SLIDER_NOT_FOUND = "SLIDER_NOT_FOUND", +} diff --git a/src/modules/businesses/entities/business.entity.ts b/src/modules/businesses/entities/business.entity.ts index 332b0aa..3a2e3ba 100644 --- a/src/modules/businesses/entities/business.entity.ts +++ b/src/modules/businesses/entities/business.entity.ts @@ -7,6 +7,7 @@ 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 { Slider } from "../../sliders/entities/slider.entity"; import { TicketCategory } from "../../tickets/entities/ticket-category.entity"; import { Ticket } from "../../tickets/entities/ticket.entity"; import { User } from "../../users/entities/user.entity"; @@ -65,6 +66,9 @@ export class Business extends BaseEntity { @OneToMany(() => Criticism, (criticism) => criticism.business) criticisms = new Collection(this); + @OneToMany(() => Slider, (slider) => slider.business) + sliders = new Collection(this); + @OneToMany(() => User, (user) => user.business) users = new Collection(this); diff --git a/src/modules/sliders/DTO/create-slider.dto.ts b/src/modules/sliders/DTO/create-slider.dto.ts new file mode 100755 index 0000000..283f686 --- /dev/null +++ b/src/modules/sliders/DTO/create-slider.dto.ts @@ -0,0 +1,40 @@ +import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"; +import { IsBoolean, IsNotEmpty, IsNumber, IsOptional, IsString, MaxLength } from "class-validator"; + +import { SliderMessage } from "../../../common/enums/message.enum"; + +export class CreateSliderDto { + @ApiProperty({ description: "Title of the slider", example: "Summer Sale" }) + @IsNotEmpty({ message: SliderMessage.TITLE_REQUIRED }) + @IsString({ message: SliderMessage.TITLE_STRING }) + @MaxLength(150, { message: SliderMessage.TITLE_MAX_LENGTH }) + title: string; + + @ApiPropertyOptional({ description: "Description of the slider", example: "Get up to 50% off" }) + @IsOptional() + @IsString({ message: SliderMessage.DESCRIPTION_STRING }) + @MaxLength(255, { message: SliderMessage.DESCRIPTION_MAX_LENGTH }) + description?: string; + + @ApiProperty({ description: "URL of the slider image", example: "https://example.com/image.jpg" }) + @IsNotEmpty({ message: SliderMessage.IMAGE_URL_REQUIRED }) + @IsString({ message: SliderMessage.IMAGE_URL_STRING }) + @MaxLength(255, { message: SliderMessage.IMAGE_URL_MAX_LENGTH }) + imageUrl: string; + + @ApiPropertyOptional({ description: "Link URL for the slider", example: "https://example.com/sale" }) + @IsOptional() + @IsString({ message: SliderMessage.LINK_STRING }) + @MaxLength(255, { message: SliderMessage.LINK_MAX_LENGTH }) + link?: string; + + @ApiPropertyOptional({ description: "Order of the slider", example: 1, default: 0 }) + @IsOptional() + @IsNumber({}, { message: SliderMessage.ORDER_NUMBER }) + order?: number; + + @ApiPropertyOptional({ description: "Whether the slider is active", example: true, default: true }) + @IsOptional() + @IsBoolean({ message: SliderMessage.IS_ACTIVE_SHOULD_BE_A_BOOLEAN }) + isActive?: boolean; +} diff --git a/src/modules/sliders/DTO/update-slider.dto.ts b/src/modules/sliders/DTO/update-slider.dto.ts new file mode 100755 index 0000000..3025f3d --- /dev/null +++ b/src/modules/sliders/DTO/update-slider.dto.ts @@ -0,0 +1,5 @@ +import { PartialType } from "@nestjs/swagger"; + +import { CreateSliderDto } from "./create-slider.dto"; + +export class UpdateSliderDto extends PartialType(CreateSliderDto) {} diff --git a/src/modules/sliders/entities/slider.entity.ts b/src/modules/sliders/entities/slider.entity.ts new file mode 100755 index 0000000..7afc756 --- /dev/null +++ b/src/modules/sliders/entities/slider.entity.ts @@ -0,0 +1,33 @@ +import { Entity, EntityRepositoryType, ManyToOne, Opt, Property } from "@mikro-orm/core"; + +import { BaseEntity } from "../../../common/entities/base.entity"; +import { Business } from "../../businesses/entities/business.entity"; +import { SliderRepository } from "../repositories/sliders.repository"; + +@Entity({ repository: () => SliderRepository }) +export class Slider extends BaseEntity { + @Property({ type: "varchar", length: 150 }) + title: string; + + @Property({ type: "varchar", length: 255, nullable: true }) + description?: string; + + @Property({ type: "varchar", length: 255 }) + imageUrl: string; + + @Property({ type: "varchar", length: 255, nullable: true }) + link?: string; + + @Property({ type: "int", default: 0 }) + order: number & Opt; + + @Property({ type: "boolean", default: true }) + isActive: boolean & Opt; + + @ManyToOne(() => Business, { deleteRule: "restrict" }) + business!: Business; + + //----------------------------- + + [EntityRepositoryType]?: SliderRepository; +} diff --git a/src/modules/sliders/providers/sliders.service.ts b/src/modules/sliders/providers/sliders.service.ts new file mode 100755 index 0000000..68f3524 --- /dev/null +++ b/src/modules/sliders/providers/sliders.service.ts @@ -0,0 +1,85 @@ +import { EntityManager } from "@mikro-orm/postgresql"; +import { BadRequestException, Injectable } from "@nestjs/common"; + +import { PaginationDto } from "../../../common/DTO/pagination.dto"; +import { CommonMessage, SliderMessage } from "../../../common/enums/message.enum"; +import { Business } from "../../businesses/entities/business.entity"; +import { CreateSliderDto } from "../DTO/create-slider.dto"; +import { UpdateSliderDto } from "../DTO/update-slider.dto"; +import { SliderRepository } from "../repositories/sliders.repository"; + +@Injectable() +export class SlidersService { + constructor( + private readonly slidersRepository: SliderRepository, + private readonly em: EntityManager, + ) {} + + async create(createSliderDto: CreateSliderDto, business: Business) { + const slider = this.slidersRepository.create({ ...createSliderDto, business }); + await this.em.persistAndFlush(slider); + return { + message: CommonMessage.CREATED, + slider, + }; + } + //*************************** */ + async getSlidersList(queryDto: PaginationDto, businessId: string) { + const [sliders, count] = await this.slidersRepository.getSlidersListForAdmin(queryDto, businessId); + return { + sliders, + count, + paginate: true, + }; + } + //************************* */ + + async getSliderListPublic(businessId: string) { + const sliders = await this.slidersRepository.find({ isActive: true, deletedAt: null, business: { id: businessId } }); + return { sliders }; + } + //************************* */ + async getSliderById(id: string, businessId: string) { + const slider = await this.slidersRepository.findOne({ id, business: { id: businessId }, deletedAt: null }); + if (!slider) throw new BadRequestException(SliderMessage.SLIDER_NOT_FOUND); + + return { + slider, + }; + } + //****************** */ + async update(id: string, updateSliderDto: UpdateSliderDto, businessId: string) { + const { slider } = await this.getSliderById(id, businessId); + + this.slidersRepository.assign(slider, updateSliderDto); + + await this.em.persistAndFlush(slider); + return { + message: CommonMessage.UPDATE_SUCCESS, + }; + } + //****************** */ + async remove(id: string, businessId: string) { + const { slider } = await this.getSliderById(id, businessId); + + slider.deletedAt = new Date(); + await this.em.flush(); + + return { + message: CommonMessage.DELETED, + }; + } + //****************** */ + + async toggleSliderStatus(id: string, businessId: string) { + const { slider } = await this.getSliderById(id, businessId); + + slider.isActive = !slider.isActive; + + await this.em.flush(); + return { + message: CommonMessage.UPDATE_SUCCESS, + }; + } + //****************** */ +} diff --git a/src/modules/sliders/repositories/sliders.repository.ts b/src/modules/sliders/repositories/sliders.repository.ts new file mode 100755 index 0000000..2e2631e --- /dev/null +++ b/src/modules/sliders/repositories/sliders.repository.ts @@ -0,0 +1,13 @@ +import { EntityRepository } from "@mikro-orm/postgresql"; + +import { PaginationDto } from "../../../common/DTO/pagination.dto"; +import { PaginationUtils } from "../../utils/providers/pagination.utils"; +import { Slider } from "../entities/slider.entity"; + +export class SliderRepository extends EntityRepository { + async getSlidersListForAdmin(queryDto: PaginationDto, businessId: string) { + const { skip, limit } = PaginationUtils(queryDto); + + return this.findAndCount({ deletedAt: null, business: { id: businessId } }, { offset: skip, limit, orderBy: { order: "ASC" } }); + } +} diff --git a/src/modules/sliders/sliders.controller.ts b/src/modules/sliders/sliders.controller.ts new file mode 100755 index 0000000..defc322 --- /dev/null +++ b/src/modules/sliders/sliders.controller.ts @@ -0,0 +1,57 @@ +import { Body, Controller, Delete, Get, Param, Patch, Post, Query, UseInterceptors } from "@nestjs/common"; +import { ApiOperation } from "@nestjs/swagger"; + +import { CreateSliderDto } from "./DTO/create-slider.dto"; +import { UpdateSliderDto } from "./DTO/update-slider.dto"; +import { SlidersService } from "./providers/sliders.service"; +import { AuthGuards } from "../../common/decorators/auth-guard.decorator"; +import { BusinessDec } from "../../common/decorators/business.decorator"; +import { SkipAuth } from "../../common/decorators/skip-auth.decorator"; +import { PaginationDto } from "../../common/DTO/pagination.dto"; +import { ParamDto } from "../../common/DTO/param.dto"; +import { BusinessInterceptor } from "../../core/interceptors/business.interceptor"; +import { Business } from "../businesses/entities/business.entity"; + +@Controller("sliders") +@AuthGuards() +@UseInterceptors(BusinessInterceptor) +export class SlidersController { + constructor(private readonly slidersService: SlidersService) {} + + @ApiOperation({ summary: "create slider (admin route)" }) + @Post() + create(@Body() createSliderDto: CreateSliderDto, @BusinessDec() business: Business) { + return this.slidersService.create(createSliderDto, business); + } + + @ApiOperation({ summary: "Get all sliders (admin route)" }) + @Get() + getSlidersList(@Query() queryDto: PaginationDto, @BusinessDec("id") businessId: string) { + return this.slidersService.getSlidersList(queryDto, businessId); + } + + @Get("list/public") + @SkipAuth() + @ApiOperation({ summary: "get all slider public" }) + getSliderListPublic(@BusinessDec("id") businessId: string) { + return this.slidersService.getSliderListPublic(businessId); + } + + @ApiOperation({ summary: "Get a slider by id (admin route)" }) + @Get(":id") + getSliderById(@Param() paramDto: ParamDto, @BusinessDec("id") businessId: string) { + return this.slidersService.getSliderById(paramDto.id, businessId); + } + + @ApiOperation({ summary: "Update a slider (admin route)" }) + @Patch(":id") + update(@Param() paramDto: ParamDto, @Body() updateSliderDto: UpdateSliderDto, @BusinessDec("id") businessId: string) { + return this.slidersService.update(paramDto.id, updateSliderDto, businessId); + } + + @ApiOperation({ summary: "Delete a slider (admin route)" }) + @Delete(":id") + remove(@Param() paramDto: ParamDto, @BusinessDec("id") businessId: string) { + return this.slidersService.remove(paramDto.id, businessId); + } +} diff --git a/src/modules/sliders/sliders.module.ts b/src/modules/sliders/sliders.module.ts new file mode 100755 index 0000000..6ee0821 --- /dev/null +++ b/src/modules/sliders/sliders.module.ts @@ -0,0 +1,15 @@ +import { MikroOrmModule } from "@mikro-orm/nestjs"; +import { Module } from "@nestjs/common"; + +import { Slider } from "./entities/slider.entity"; +import { SlidersService } from "./providers/sliders.service"; +import { SlidersController } from "./sliders.controller"; +import { BusinessesModule } from "../businesses/businesses.module"; + +@Module({ + imports: [MikroOrmModule.forFeature([Slider]), BusinessesModule], + controllers: [SlidersController], + providers: [SlidersService], + exports: [SlidersService], +}) +export class SlidersModule {} diff --git a/src/modules/users/services/users.service.ts b/src/modules/users/services/users.service.ts index d751bff..da9ab1f 100644 --- a/src/modules/users/services/users.service.ts +++ b/src/modules/users/services/users.service.ts @@ -94,22 +94,22 @@ export class UsersService { /*******************************/ - async updateUser(id: string, updateData: Partial, em: EntityManager) { - const user = await em.findOne(User, { id, deletedAt: null }); + async updateUser(userId: string, updateData: Partial, em: EntityManager) { + const user = await em.findOne(User, { id: userId, deletedAt: null }); if (!user) throw new BadRequestException(UserMessage.USER_NOT_FOUND); if (updateData.phone) { - const existingUser = await em.findOne(User, { phone: updateData.phone, deletedAt: null, id: { $ne: id } }); + const existingUser = await em.findOne(User, { phone: updateData.phone, deletedAt: null, id: { $ne: userId } }); if (existingUser) throw new BadRequestException(UserMessage.PHONE_EXIST); } if (updateData.email) { - const existingUser = await em.findOne(User, { email: updateData.email, deletedAt: null, id: { $ne: id } }); + const existingUser = await em.findOne(User, { email: updateData.email, deletedAt: null, id: { $ne: userId } }); if (existingUser) throw new BadRequestException(UserMessage.EMAIL_EXIST); } if (updateData.nationalCode) { - const existingUser = await em.findOne(User, { nationalCode: updateData.nationalCode, deletedAt: null, id: { $ne: id } }); + const existingUser = await em.findOne(User, { nationalCode: updateData.nationalCode, deletedAt: null, id: { $ne: userId } }); if (existingUser) throw new BadRequestException(UserMessage.NATIONAL_CODE_EXIST); }