diff --git a/src/common/enums/message.enum.ts b/src/common/enums/message.enum.ts index c8b1851..0f2206f 100755 --- a/src/common/enums/message.enum.ts +++ b/src/common/enums/message.enum.ts @@ -448,6 +448,7 @@ export const enum LearningMessage { CATEGORY_NOT_FOUND = "دسته بندی با این مشخصات یافت نشد", CATEGORY_MUST_BE_ID = "دسته‌بندی باید شناسه باشد", CATEGORY_MUST_BE_UUID = "دسته‌بندی باید UUID باشد", + CATEGORY_NAME_EXIST = "دسته بندی با این نام قبلا ثبت شده است", } export const enum DiscountMessage { diff --git a/src/modules/danak-services/providers/danak-services.service.ts b/src/modules/danak-services/providers/danak-services.service.ts index 5cb5417..74dda6a 100755 --- a/src/modules/danak-services/providers/danak-services.service.ts +++ b/src/modules/danak-services/providers/danak-services.service.ts @@ -259,6 +259,8 @@ export class DanakServicesService { const serviceQueryBuilder = this.danakServicesRepository .createQueryBuilder("service") .where("service.deletedAt IS NULL") + .leftJoin("service.category", "category") + .addSelect(["category.id", "category.title"]) .leftJoinAndSelect("service.images", "images") .leftJoinAndSelect("service.subscriptionPlans", "subscriptionPlans") .leftJoin("service.reviews", "reviews", "reviews.status = :reviewStatus", { reviewStatus: ReviewStatus.APPROVED }) @@ -286,7 +288,7 @@ export class DanakServicesService { // "averageRating", // ) .andWhere("service.id = :serviceId", { serviceId }) - .groupBy("service.id, images.id, subscriptionPlans.id, reviews.id, user.id"); + .groupBy("service.id,category.id, images.id, subscriptionPlans.id, reviews.id, user.id"); if (!isAdmin) { serviceQueryBuilder diff --git a/src/modules/learnings/DTO/create-learning.dto.ts b/src/modules/learnings/DTO/create-learning.dto.ts index 0bc4b8a..59f5693 100755 --- a/src/modules/learnings/DTO/create-learning.dto.ts +++ b/src/modules/learnings/DTO/create-learning.dto.ts @@ -1,40 +1,37 @@ import { ApiProperty } from "@nestjs/swagger"; -import { IsNotEmpty, IsOptional, IsString, IsUUID } from "class-validator"; +import { IsNotEmpty, IsNumberString, IsString, IsUUID, IsUrl } from "class-validator"; import { LearningMessage } from "../../../common/enums/message.enum"; export class CreateLearningDto { + @IsNotEmpty({ message: LearningMessage.CATEGORY_MUST_BE_ID }) + @IsUUID("4", { message: LearningMessage.CATEGORY_MUST_BE_UUID }) + @ApiProperty({ description: "Category ID", example: "d290f1ee-6c54-4b01-90e6-d701748f0851" }) + categoryId: string; + @ApiProperty({ description: "Title of the learning", maxLength: 200 }) @IsString({ message: LearningMessage.TITLE_STRING }) @IsNotEmpty({ message: LearningMessage.TITLE_REQUIRED }) title: string; - @ApiProperty({ description: "Description of the learning" }) - @IsString({ message: LearningMessage.DESCRIPTION_STRING }) @IsNotEmpty({ message: LearningMessage.DESCRIPTION_REQUIRED }) + @IsString({ message: LearningMessage.DESCRIPTION_STRING }) + @ApiProperty({ description: "Description of the learning" }) description: string; - @ApiProperty({ description: "Cover image URL" }) - @IsString({ message: LearningMessage.COVER_URL_INVALID }) @IsNotEmpty({ message: LearningMessage.COVER_URL_REQUIRED }) + @IsUrl({ protocols: ["http", "https"], require_protocol: true }, { message: LearningMessage.COVER_URL_INVALID }) + @IsString({ message: LearningMessage.COVER_URL_INVALID }) + @ApiProperty({ description: "Cover image URL" }) coverUrl: string; + @IsString({ message: LearningMessage.VIDEO_URL_INVALID }) + @IsUrl({ protocols: ["http", "https"], require_protocol: true }, { message: LearningMessage.VIDEO_URL_INVALID }) @ApiProperty({ description: "Video URL", required: false }) - @IsString({ message: LearningMessage.VIDEO_URL_INVALID }) - @IsOptional() - videoUrl?: string; + videoUrl: string; + @IsNotEmpty({ message: LearningMessage.VIDEO_DURATION_INVALID }) + @IsNumberString({ no_symbols: true }, { message: LearningMessage.VIDEO_DURATION_INVALID }) @ApiProperty({ description: "Video Duration", required: false }) - @IsString({ message: LearningMessage.VIDEO_URL_INVALID }) - @IsOptional() - videoDuration?: string; - - @IsOptional() - @IsNotEmpty({ message: LearningMessage.CATEGORY_MUST_BE_ID }) - @IsUUID("4", { message: LearningMessage.CATEGORY_MUST_BE_UUID }) - @ApiProperty({ - description: "Category ID", - example: "d290f1ee-6c54-4b01-90e6-d701748f0851", - }) - categoryId: string; + videoDuration: string; } diff --git a/src/modules/learnings/DTO/update-learning-category.dto.ts b/src/modules/learnings/DTO/update-learning-category.dto.ts new file mode 100644 index 0000000..b251a21 --- /dev/null +++ b/src/modules/learnings/DTO/update-learning-category.dto.ts @@ -0,0 +1,5 @@ +import { PartialType } from "@nestjs/swagger"; + +import { CreateLearningCategoryDto } from "./create-learning-category.dto"; + +export class UpdateLearningCategoryDto extends PartialType(CreateLearningCategoryDto) {} diff --git a/src/modules/learnings/DTO/update-learning.dto.ts b/src/modules/learnings/DTO/update-learning.dto.ts new file mode 100644 index 0000000..c35b5f2 --- /dev/null +++ b/src/modules/learnings/DTO/update-learning.dto.ts @@ -0,0 +1,5 @@ +import { PartialType } from "@nestjs/swagger"; + +import { CreateLearningDto } from "./create-learning.dto"; + +export class UpdateLearningDto extends PartialType(CreateLearningDto) {} diff --git a/src/modules/learnings/entities/learning-category.entity.ts b/src/modules/learnings/entities/learning-category.entity.ts index cc2da5d..d121227 100755 --- a/src/modules/learnings/entities/learning-category.entity.ts +++ b/src/modules/learnings/entities/learning-category.entity.ts @@ -1,13 +1,16 @@ -import { Column, Entity, OneToMany } from "typeorm"; +import { Column, DeleteDateColumn, Entity, OneToMany } from "typeorm"; import { Learning } from "./learning.entity"; import { BaseEntity } from "../../../common/entities/base.entity"; @Entity() export class LearningCategory extends BaseEntity { - @Column({ type: "varchar", length: 100 }) + @Column({ type: "varchar", length: 100, unique: true }) name: string; @OneToMany(() => Learning, (learning) => learning.category) learnings: Learning[]; + + @DeleteDateColumn() + deletedAt?: Date; } diff --git a/src/modules/learnings/entities/learning-progress.entity.ts b/src/modules/learnings/entities/learning-progress.entity.ts index 5348c4c..eaef21a 100755 --- a/src/modules/learnings/entities/learning-progress.entity.ts +++ b/src/modules/learnings/entities/learning-progress.entity.ts @@ -12,6 +12,6 @@ export class LearningProgress extends BaseEntity { @ManyToOne(() => Learning, (learning) => learning.learningProgress) learning: Learning; - @Column({ type: "timestamp", default: () => "CURRENT_TIMESTAMP" }) + @Column({ type: "timestamptz", default: () => "CURRENT_TIMESTAMP" }) watchedAt: Date; } diff --git a/src/modules/learnings/entities/learning.entity.ts b/src/modules/learnings/entities/learning.entity.ts index fb1ed14..8a5af7e 100755 --- a/src/modules/learnings/entities/learning.entity.ts +++ b/src/modules/learnings/entities/learning.entity.ts @@ -1,4 +1,4 @@ -import { Column, Entity, ManyToOne, OneToMany } from "typeorm"; +import { Column, DeleteDateColumn, Entity, ManyToOne, OneToMany } from "typeorm"; import { LearningCategory } from "./learning-category.entity"; import { LearningProgress } from "./learning-progress.entity"; @@ -26,4 +26,7 @@ export class Learning extends BaseEntity { @OneToMany(() => LearningProgress, (learningProgress) => learningProgress.learning) learningProgress: LearningProgress[]; + + @DeleteDateColumn() + deletedAt?: Date; } diff --git a/src/modules/learnings/learning.controller.ts b/src/modules/learnings/learning.controller.ts index 10649c7..0d8a199 100755 --- a/src/modules/learnings/learning.controller.ts +++ b/src/modules/learnings/learning.controller.ts @@ -1,8 +1,9 @@ -import { Body, Controller, Get, Param, Post } from "@nestjs/common"; +import { Body, Controller, Delete, Get, Param, Patch, Post } from "@nestjs/common"; import { ApiOperation, ApiTags } from "@nestjs/swagger"; import { CreateLearningCategoryDto } from "./DTO/create-learning-category.dto"; import { CreateLearningDto } from "./DTO/create-learning.dto"; +import { UpdateLearningCategoryDto } from "./DTO/update-learning-category.dto"; import { LearningService } from "./providers/learning.service"; import { AuthGuards } from "../../common/decorators/auth-guard.decorator"; import { PermissionsDec } from "../../common/decorators/permission.decorator"; @@ -15,16 +16,26 @@ import { PermissionEnum } from "../users/enums/permission.enum"; export class LearningController { constructor(private readonly learningService: LearningService) {} - //************************ */ - - @ApiOperation({ summary: "Create learning" }) + @ApiOperation({ summary: "Create learning video" }) @PermissionsDec(PermissionEnum.LEARNINGS) @Post() - create(@Body() createDto: CreateLearningDto) { - return this.learningService.createLearning(createDto); + createLearningVideo(@Body() createDto: CreateLearningDto) { + return this.learningService.createLearningVideo(createDto); } - //************************ */ + @ApiOperation({ summary: "find learnings video" }) + @Get() + findAllLearnings() { + return this.learningService.findLearningsVideos(); + } + + @ApiOperation({ summary: "find learning video by id" }) + @Get(":id") + findLearningVideoById(@Param() paramDto: ParamDto) { + return this.learningService.findLearningVideoById(paramDto.id); + } + + //----------------- category management ---------------------- @ApiOperation({ summary: "Create learning category" }) @PermissionsDec(PermissionEnum.LEARNINGS) @@ -33,37 +44,29 @@ export class LearningController { return this.learningService.createLearningCategory(createDto); } - //************************ */ - - @ApiOperation({ summary: "find all learnings" }) - @Get() - findAllLearnings() { - return this.learningService.findAllLearnings(); - } - - //************************ */ - - @ApiOperation({ summary: "find one learning by id" }) - @Get(":id") - findOneLearning(@Param() paramDto: ParamDto) { - return this.learningService.findOneLearning(paramDto.id); - } - - //************************ */ - - @ApiOperation({ summary: "find all learning category" }) + @ApiOperation({ summary: "find learning categories" }) @Get("category") - findAllCategories() { - return this.learningService.findAllCategories(); + findCategories() { + return this.learningService.findCategories(); } - //************************ */ - @ApiOperation({ summary: "find one learning category by id" }) @Get("category/:id") - findOneCategory(@Param() paramDto: ParamDto) { - return this.learningService.findOneCategory(paramDto.id); + findCategoryById(@Param() paramDto: ParamDto) { + return this.learningService.findCategoryById(paramDto.id); } - //************************ */ + @ApiOperation({ summary: "update learning category ==> admin route" }) + @PermissionsDec(PermissionEnum.LEARNINGS) + @Patch("category/:id") + updateLearningCategory(@Param() paramDto: ParamDto, @Body() updateDto: UpdateLearningCategoryDto) { + return this.learningService.updateLearningCategory(paramDto.id, updateDto); + } + + @ApiOperation({ summary: "delete category by id" }) + @PermissionsDec(PermissionEnum.LEARNINGS) + @Delete("category/:id") + deleteCategoryById(@Param() paramDto: ParamDto) { + return this.learningService.deleteCategoryById(paramDto.id); + } } diff --git a/src/modules/learnings/providers/learning.service.ts b/src/modules/learnings/providers/learning.service.ts index 33f27bc..6d1788c 100755 --- a/src/modules/learnings/providers/learning.service.ts +++ b/src/modules/learnings/providers/learning.service.ts @@ -1,9 +1,12 @@ import { BadRequestException, Injectable } from "@nestjs/common"; +import { IsNull, Not } from "typeorm"; import { CommonMessage, LearningMessage } from "../../../common/enums/message.enum"; import { UsersService } from "../../users/providers/users.service"; import { CreateLearningCategoryDto } from "../DTO/create-learning-category.dto"; import { CreateLearningDto } from "../DTO/create-learning.dto"; +import { UpdateLearningCategoryDto } from "../DTO/update-learning-category.dto"; +import { UpdateLearningDto } from "../DTO/update-learning.dto"; import { LearningCategoryRepository } from "../repositories/learning-category.repository"; import { LearningProgressRepository } from "../repositories/learning-progress.repository"; import { LearningRepository } from "../repositories/learning.repository"; @@ -17,15 +20,12 @@ export class LearningService { private readonly usersService: UsersService, ) {} - //******************** */ + //********************************************* */ - async createLearning(createDto: CreateLearningDto) { - const { category } = await this.findOneCategory(createDto.categoryId); + async createLearningVideo(createDto: CreateLearningDto) { + const { category } = await this.findCategoryById(createDto.categoryId); - const learning = this.learningRepository.create({ - ...createDto, - category, - }); + const learning = this.learningRepository.create({ ...createDto, category }); await this.learningRepository.save(learning); return { @@ -33,74 +33,113 @@ export class LearningService { learning, }; } + //********************************************* */ - //******************** */ + async updateLearningVideo(learningId: string, updateDto: UpdateLearningDto) { + const { learning } = await this.findLearningVideoById(learningId); - async findAllLearnings() { - const learnings = await this.learningRepository.find(); + if (updateDto.categoryId) { + const { category } = await this.findCategoryById(updateDto.categoryId); + learning.category = category; + } + + await this.learningRepository.save({ ...learning, ...updateDto }); + return { + message: CommonMessage.UPDATE_SUCCESS, + learning, + }; + } + //********************************************* */ + + async deleteLearningVideoById(learningId: string) { + await this.findLearningVideoById(learningId); + await this.learningRepository.softDelete(learningId); + + return { + message: CommonMessage.DELETED, + }; + } + //********************************************* */ + + async findLearningsVideos() { + const learnings = await this.learningRepository.findBy({ deletedAt: IsNull() }); return { learnings }; } + //********************************************* */ - //******************** */ - - async findOneLearning(id: string) { - const learning = await this.learningRepository.findOneBy({ - id, - }); + async findLearningVideoById(id: string) { + const learning = await this.learningRepository.findOneBy({ id, deletedAt: IsNull() }); if (!learning) throw new BadRequestException(LearningMessage.NOT_FOUND); return { learning }; } - - //******************** */ + //********************************************* */ async createLearningCategory(createDto: CreateLearningCategoryDto) { + const existCategory = await this.learningCategoryRepository.findOneBy({ name: createDto.name }); + if (existCategory) throw new BadRequestException(LearningMessage.CATEGORY_NAME_EXIST); + const category = this.learningCategoryRepository.create(createDto); await this.learningCategoryRepository.save(category); return { category }; } + //********************************************* */ - //******************** */ + async updateLearningCategory(categoryId: string, updateDto: UpdateLearningCategoryDto) { + const category = await this.learningCategoryRepository.findOneBy({ id: categoryId }); + if (!category) throw new BadRequestException(LearningMessage.CATEGORY_NOT_FOUND); - async findAllCategories() { - const categories = await this.learningCategoryRepository.find(); + if (updateDto.name) { + const existCategory = await this.learningCategoryRepository.findOneBy({ name: updateDto.name, id: Not(categoryId) }); + if (existCategory) throw new BadRequestException(LearningMessage.CATEGORY_NAME_EXIST); + category.name = updateDto.name; + } + + await this.learningCategoryRepository.save({ ...category }); + return { + message: CommonMessage.UPDATE_SUCCESS, + category, + }; + } + //********************************************* */ + + async deleteCategoryById(categoryId: string) { + await this.findCategoryById(categoryId); + + await this.learningCategoryRepository.softDelete(categoryId); + return { + message: CommonMessage.DELETED, + }; + } + //********************************************* */ + + async findCategories() { + const categories = await this.learningCategoryRepository.findBy({ deletedAt: IsNull() }); return { categories }; } + //********************************************* */ - //******************** */ - - async findOneCategory(id: string) { - const category = await this.learningCategoryRepository.findOneBy({ - id, - }); + async findCategoryById(id: string) { + const category = await this.learningCategoryRepository.findOneBy({ id, deletedAt: IsNull() }); if (!category) throw new BadRequestException(LearningMessage.CATEGORY_NOT_FOUND); return { category }; } - - //******************** */ + //********************************************* */ async trackProgress(userId: string, learningId: string) { const { user } = await this.usersService.findOneById(userId); - const { learning } = await this.findOneLearning(learningId); + const { learning } = await this.findLearningVideoById(learningId); - const progress = this.learningProgressRepository.create({ - user, - learning, - watchedAt: new Date(), - }); + const progress = this.learningProgressRepository.create({ user, learning }); return this.learningProgressRepository.save(progress); } - - //******************** */ + //********************************************* */ async getUserProgress(userId: string) { - return this.learningProgressRepository.find({ - where: { user: { id: userId } }, - relations: ["learning"], - }); + return this.learningProgressRepository.find({ where: { user: { id: userId } }, relations: { learning: true } }); } }