This commit is contained in:
mahyargdz
2025-09-14 15:15:03 +03:30
commit be82059172
534 changed files with 51310 additions and 0 deletions
@@ -0,0 +1,49 @@
import { Expose, plainToClass } from "class-transformer";
import { IsNotEmpty, IsOptional, IsString } from "class-validator";
import { ApiProperty } from "../../../common/decorator/swggerDocs";
import { IsValidId } from "../../../common/decorator/validation.decorator";
import { ILearning } from "../model/Abstractions/ILearning";
import { LearningCategoryModel } from "../model/learningCategory.model";
export class CreateLearningDTO {
@Expose()
@IsNotEmpty()
@IsString()
@ApiProperty({ type: "string", description: "title of learning", example: "عنوان ۱" })
title: string;
@Expose()
@IsNotEmpty()
@IsString()
@ApiProperty({ type: "string", description: "description of learning", example: "توضیحات ویدیو" })
description: string;
@Expose()
@IsNotEmpty()
@IsString()
@ApiProperty({ type: "string", description: "video url of learning", example: "https://loremflickr.com/454/340" })
videoUrl: string;
@Expose()
@IsOptional()
@IsNotEmpty()
@IsString()
@ApiProperty({ type: "string", description: "cover url of learning", example: "https://loremflickr.com/454/340" })
cover?: string;
@Expose()
@IsNotEmpty()
@IsValidId(LearningCategoryModel)
@ApiProperty({ type: "string", description: "id of learning category", example: "66ed83563858f185c68449ff" })
category: string;
public static transformLearning(data: ILearning): CreateLearningDTO {
const LearningDto = plainToClass(CreateLearningDTO, data, {
excludeExtraneousValues: true,
enableImplicitConversion: true,
});
return LearningDto;
}
}
@@ -0,0 +1,12 @@
import { Expose } from "class-transformer";
import { IsNotEmpty, IsString } from "class-validator";
import { ApiProperty } from "../../../common/decorator/swggerDocs";
export class CreateLearningCategoryDTO {
@Expose()
@IsNotEmpty()
@IsString()
@ApiProperty({ type: "string", description: "title of learning", example: "عنوان ۱" })
title: string;
}
@@ -0,0 +1,14 @@
import { Expose } from "class-transformer";
import { IsNotEmpty } from "class-validator";
import { ApiProperty } from "../../../common/decorator/swggerDocs";
import { IsValidId } from "../../../common/decorator/validation.decorator";
import { LearningModel } from "../model/learning.model";
export class CreateLearningProgressDTO {
@Expose()
@IsNotEmpty()
@IsValidId(LearningModel)
@ApiProperty({ type: "string", description: "id of learning video", example: "66ed83563858f185c68449ff" })
videoId: string;
}
@@ -0,0 +1,18 @@
import { Expose } from "class-transformer";
import { IsOptional, IsString } from "class-validator";
import { ApiProperty } from "../../../common/decorator/swggerDocs";
export class UpdateLearningDTO {
@Expose()
@IsOptional()
@IsString()
@ApiProperty({ type: "string", description: "title of learning", example: "عنوان ۱" })
title: string;
@Expose()
@IsOptional()
@IsString()
@ApiProperty({ type: "string", description: "description of learning", example: "توضیحات ویدیو" })
description: string;
}
@@ -0,0 +1,12 @@
import { Expose } from "class-transformer";
import { IsOptional, IsString } from "class-validator";
import { ApiProperty } from "../../../common/decorator/swggerDocs";
export class UpdateLearningCategoryDTO {
@Expose()
@IsOptional()
@IsString()
@ApiProperty({ type: "string", description: "title of learning category", example: "عنوان ۱" })
title: string;
}
@@ -0,0 +1,76 @@
import { Request } from "express";
import { inject } from "inversify";
import { controller, httpGet, httpPost, request, requestBody, requestParam } from "inversify-express-utils";
import { LearningService } from "./learning.service";
import { HttpStatus } from "../../common";
import { CreateLearningProgressDTO } from "./DTO/createLearningProgress.dto";
import { BaseController } from "../../common/base/controller";
import { ApiAuth, ApiModel, ApiOperation, ApiParam, ApiResponse, ApiTags } from "../../common/decorator/swggerDocs";
import { Guard } from "../../core/middlewares/guard.middleware";
import { ValidationMiddleware } from "../../core/middlewares/validator.middleware";
import { IOCTYPES } from "../../IOC/ioc.types";
import { ISeller } from "../seller/models/Abstraction/ISeller";
@controller("/learning")
@ApiTags("Learning")
export class LearningController extends BaseController {
@inject(IOCTYPES.LearningService) learningService: LearningService;
@ApiOperation("get a list of learning")
@ApiResponse("successful", HttpStatus.Ok)
@httpGet("")
public async getAllLearning() {
const data = await this.learningService.getAllLearning();
return this.response(data, HttpStatus.Ok);
}
@ApiOperation("get a list of learning")
@ApiResponse("successful", HttpStatus.Ok)
@ApiAuth()
@httpGet("/seller", Guard.authSeller())
public async getAllLearningByCategories(@request() req: Request) {
const seller = req.user as ISeller;
const data = await this.learningService.getAllLearningByCategory(seller._id.toString());
return this.response(data, HttpStatus.Ok);
}
@ApiOperation("get a list of learning category")
@ApiResponse("successful", HttpStatus.Ok)
@httpGet("/category")
public async getAllLearningCategory() {
const data = await this.learningService.getAllLearningCategory();
return this.response(data, HttpStatus.Ok);
}
@ApiOperation("get a learning")
@ApiResponse("successful", HttpStatus.Ok)
@ApiParam("id", "the learning id", true)
@httpGet("/:id")
public async getLearning(@requestParam("id") learningId: string) {
const data = await this.learningService.getById(learningId);
return this.response(data, HttpStatus.Ok);
}
@ApiOperation("learning progress for seller ===> need to login as seller")
@ApiResponse("successful", HttpStatus.Ok)
@ApiModel(CreateLearningProgressDTO)
@ApiAuth()
@httpPost("/progress", Guard.authSeller(), ValidationMiddleware.validateInput(CreateLearningProgressDTO))
public async sellerPanel(@request() req: Request, @requestBody() createLearningProgressDto: CreateLearningProgressDTO) {
const seller = req.user as ISeller;
const data = await this.learningService.createLearningProgress(seller._id.toString(), createLearningProgressDto.videoId);
return this.response(data);
}
@ApiOperation("get a list of learning video watched")
@ApiResponse("successful", HttpStatus.Ok)
@ApiParam("id", "the category id", true)
@ApiAuth()
@httpGet("/:id/seller", Guard.authSeller())
public async getVideoWatchedStatus(@request() req: Request, @requestParam("id") catId: string) {
const seller = req.user as ISeller;
const data = await this.learningService.getLearningProgressWatched(seller._id.toString(), catId);
return this.response(data, HttpStatus.Ok);
}
}
+140
View File
@@ -0,0 +1,140 @@
import { inject, injectable } from "inversify";
import { isValidObjectId } from "mongoose";
import { CreateLearningDTO } from "./DTO/createLearning.dto";
import { CreateLearningCategoryDTO } from "./DTO/createLearningCategory.dto";
import { UpdateLearningDTO } from "./DTO/updateLearning.dto";
import { UpdateLearningCategoryDTO } from "./DTO/updateLearningCategory.dto";
import { LearningRepo } from "./repository/learning";
import { LearningCategoryRepo } from "./repository/learningCategory";
import { LearningProgressRepo } from "./repository/learningProgress";
import { CommonMessage, SellerMessage } from "../../common/enums/message.enum";
import { BadRequestError } from "../../core/app/app.errors";
import { IOCTYPES } from "../../IOC/ioc.types";
@injectable()
export class LearningService {
@inject(IOCTYPES.LearningRepo) learningRepo: LearningRepo;
@inject(IOCTYPES.LearningProgressRepo) learningProgressRepo: LearningProgressRepo;
@inject(IOCTYPES.LearningCategoryRepo) learningCategoryRepo: LearningCategoryRepo;
async createLearning(createDto: CreateLearningDTO) {
const category = await this.learningCategoryRepo.model.findOne({ _id: createDto.category, deleted: false });
if (!category) {
throw new BadRequestError(CommonMessage.NotFoundById);
}
const learning = await this.learningRepo.model.create({
...createDto,
category,
});
return {
message: CommonMessage.Created,
learning,
};
}
async createLearningCategory(createDto: CreateLearningCategoryDTO) {
const learningCategory = await this.learningCategoryRepo.model.create({
...createDto,
});
return {
message: CommonMessage.Created,
learningCategory,
};
}
async getAllLearning() {
const learning = await this.learningRepo.model.find({ deleted: false }).populate("category");
return { learning };
}
async getAllLearningByCategory(sellerId: string) {
const learning = await this.learningRepo.getVideosGroupByCategories(sellerId);
return { learning };
}
async getAllLearningCategory() {
const learningCategory = await this.learningCategoryRepo.model.find({
deleted: false,
});
return { learningCategory };
}
async getById(learningId: string) {
if (!isValidObjectId(learningId)) throw new BadRequestError(CommonMessage.NotValidId);
const learning = await this.learningRepo.model.findById(learningId).populate("category");
if (!learning) throw new BadRequestError(CommonMessage.NotFoundById);
return {
learning,
};
}
async update(learningId: string, updateDto: UpdateLearningDTO) {
console.log(learningId);
if (!isValidObjectId(learningId)) throw new BadRequestError(CommonMessage.NotValidId);
const learning = await this.learningRepo.model.findByIdAndUpdate(learningId, updateDto, { new: true });
console.log(learning);
if (!learning) throw new BadRequestError(CommonMessage.NotFoundById);
return {
message: CommonMessage.Updated,
learning,
};
}
async updateCategory(catId: string, updateDto: UpdateLearningCategoryDTO) {
if (!isValidObjectId(catId)) throw new BadRequestError(CommonMessage.NotValidId);
const learningCategory = await this.learningCategoryRepo.model.findByIdAndUpdate(catId, updateDto, { new: true });
console.log(learningCategory);
if (!learningCategory) throw new BadRequestError(CommonMessage.NotFoundById);
return {
message: CommonMessage.Updated,
learningCategory,
};
}
async delete(learningId: string) {
if (!isValidObjectId(learningId)) throw new BadRequestError(CommonMessage.NotValidId);
await this.learningRepo.model.findByIdAndUpdate(learningId, { deleted: true });
return {
message: CommonMessage.Deleted,
};
}
async createLearningProgress(sellerId: string, videoId: string) {
const learningVideo = await this.learningRepo.findById(videoId);
if (!learningVideo) {
throw new BadRequestError(CommonMessage.NotFoundById);
}
const learningProgress = await this.learningProgressRepo.model.findOne({
sellerId,
videoId,
});
if (learningProgress) throw new BadRequestError(SellerMessage.VideoWasWached);
const newLearningProgress = await this.learningProgressRepo.model.create({
sellerId,
videoId: learningVideo._id,
watchedAt: new Date(),
});
return {
message: CommonMessage.Created,
newLearningProgress,
};
}
async getLearningProgressBySellerForAdmin() {
const progress = await this.learningProgressRepo.getLearningProgressBySellers();
console.log(progress);
return { progress };
}
async getLearningProgressWatched(sellerId: string, catId: string) {
const progress = await this.learningRepo.getVideosWithWatchStatus(sellerId, catId);
console.log(progress);
return { progress };
}
}
@@ -0,0 +1,10 @@
import { Schema } from "mongoose";
export interface ILearning {
title: string;
description: string;
videoUrl: string;
cover: string;
category: Schema.Types.ObjectId;
deleted: boolean;
}
@@ -0,0 +1,4 @@
export interface ILearningCategory {
title: string;
deleted: boolean;
}
@@ -0,0 +1,7 @@
import { Schema } from "mongoose";
export interface ILearningProgress {
sellerId: Schema.Types.ObjectId;
videoId: Schema.Types.ObjectId;
watchedAt?: Date;
}
@@ -0,0 +1,23 @@
import { Schema, model } from "mongoose";
import { ILearning } from "./Abstractions/ILearning";
const LearningSchema = new Schema<ILearning>(
{
title: { type: String, required: true },
description: { type: String, required: true },
videoUrl: { type: String, required: true },
cover: { type: String, required: true },
category: { type: Schema.Types.ObjectId, ref: "LearningCategory", required: true },
deleted: { type: Boolean, default: false },
},
{
timestamps: true,
toJSON: { versionKey: false, virtuals: true },
id: false,
},
);
const LearningModel = model<ILearning>("Learning", LearningSchema);
export { LearningModel };
@@ -0,0 +1,19 @@
import { Schema, model } from "mongoose";
import { ILearningCategory } from "./Abstractions/ILearningCategory";
const LearningCategorySchema = new Schema<ILearningCategory>(
{
title: { type: String, required: true, unique: true },
deleted: { type: Boolean, default: false },
},
{
timestamps: true,
toJSON: { versionKey: false, virtuals: true },
id: false,
},
);
const LearningCategoryModel = model<ILearningCategory>("LearningCategory", LearningCategorySchema);
export { LearningCategoryModel };
@@ -0,0 +1,23 @@
import { Schema, model } from "mongoose";
import { ILearningProgress } from "./Abstractions/ILearningProgress";
const LearningProgressSchema = new Schema<ILearningProgress>(
{
sellerId: { type: Schema.Types.ObjectId, ref: "Seller", required: true },
videoId: { type: Schema.Types.ObjectId, ref: "Learning", required: true },
watchedAt: { type: Date },
},
{
timestamps: true,
toJSON: { versionKey: false, virtuals: true },
id: false,
},
);
// Defining a unique compound index for sellerId and videoId
LearningProgressSchema.index({ sellerId: 1, videoId: 1 }, { unique: true });
const LearningProgressModel = model<ILearningProgress>("LearningProgress", LearningProgressSchema);
export { LearningProgressModel };
+127
View File
@@ -0,0 +1,127 @@
import { Types } from "mongoose";
import { BaseRepository } from "../../../common/base/repository";
import { ILearning } from "../model/Abstractions/ILearning";
import { LearningModel } from "../model/learning.model";
export class LearningRepo extends BaseRepository<ILearning> {
constructor() {
super(LearningModel);
}
async getVideosWithWatchStatus(sellerId: string, catId: string) {
const result = await LearningModel.aggregate([
{
$match: {
category: catId,
deleted: false,
},
},
{
$lookup: {
from: "learningprogresses",
let: { seller: new Types.ObjectId(sellerId), videoId: "$_id" },
pipeline: [
{
$match: {
$expr: {
$and: [{ $eq: ["$sellerId", "$$seller"] }, { $eq: ["$videoId", "$$videoId"] }],
},
},
},
],
as: "watchStatus",
},
},
{
$addFields: {
isWatched: { $gt: [{ $size: { $ifNull: ["$watchStatus", []] } }, 0] },
},
},
{
$project: {
_id: 1,
title: 1,
description: 1,
videoUrl: 1,
cover: 1,
isWatched: 1,
},
},
]);
return result;
}
async getVideosGroupByCategories(sellerId: string) {
const result = await LearningModel.aggregate([
{
$match: {
deleted: false,
},
},
{
$lookup: {
from: "learningprogresses",
let: { seller: new Types.ObjectId(sellerId), videoId: "$_id" },
pipeline: [
{
$match: {
$expr: {
$and: [{ $eq: ["$sellerId", "$$seller"] }, { $eq: ["$videoId", "$$videoId"] }],
},
},
},
],
as: "watchStatus",
},
},
{
$addFields: {
isWatched: { $gt: [{ $size: { $ifNull: ["$watchStatus", []] } }, 0] },
},
},
{
$lookup: {
from: "learningcategories",
localField: "category",
foreignField: "_id",
as: "categoryDetails",
},
},
{
$unwind: "$categoryDetails",
},
{
$group: {
_id: "$category",
categoryTitle: { $first: "$categoryDetails.title" },
videos: {
$push: {
_id: "$_id",
title: "$title",
cover: "$cover",
description: "$description",
videoUrl: "$videoUrl",
isWatched: "$isWatched",
},
},
},
},
{
$project: {
_id: 0,
category: "$_id",
categoryTitle: 1,
videos: 1,
},
},
]);
return result;
}
}
export function CreateLearningRepo(): LearningRepo {
return new LearningRepo();
}
@@ -0,0 +1,13 @@
import { BaseRepository } from "../../../common/base/repository";
import { ILearningCategory } from "../model/Abstractions/ILearningCategory";
import { LearningCategoryModel } from "../model/learningCategory.model";
export class LearningCategoryRepo extends BaseRepository<ILearningCategory> {
constructor() {
super(LearningCategoryModel);
}
}
export function CreateLearningCategoryRepo(): LearningCategoryRepo {
return new LearningCategoryRepo();
}
@@ -0,0 +1,105 @@
import { BaseRepository } from "../../../common/base/repository";
import { ILearningProgress } from "../model/Abstractions/ILearningProgress";
import { LearningProgressModel } from "../model/learningProgress.model";
export class LearningProgressRepo extends BaseRepository<ILearningProgress> {
constructor() {
super(LearningProgressModel);
}
async getLearningProgressBySellers() {
const result = await LearningProgressModel.aggregate([
{
$lookup: {
from: "learnings",
localField: "videoId",
foreignField: "_id",
as: "videoDetails",
},
},
{
$lookup: {
from: "sellers",
localField: "sellerId",
foreignField: "_id",
as: "sellerDetails",
},
},
{
$lookup: {
from: "shops",
localField: "sellerDetails._id",
foreignField: "owner",
as: "shopDetails",
},
},
{
$unwind: "$shopDetails",
},
{
$addFields: {
"sellerDetails.shopName": "$shopDetails.shopName",
"sellerDetails.shopId": "$shopDetails._id",
},
},
{
$project: {
_id: 0,
sellerId: 1,
videoId: 1,
watchedAt: 1,
videoDetails: { $arrayElemAt: ["$videoDetails", 0] },
sellerDetails: { $arrayElemAt: ["$sellerDetails", 0] },
},
},
{
$group: {
_id: "$sellerId",
sellerDetails: { $first: "$sellerDetails" },
watchedVideos: {
$push: {
videoId: "$videoId",
videoDetails: "$videoDetails",
watchedAt: "$watchedAt",
},
},
},
},
{
$lookup: {
from: "learnings",
pipeline: [],
as: "allVideos",
},
},
{
$project: {
sellerDetails: {
_id: 1,
fullName: 1,
shopName: 1,
shopId: 1,
},
watchedVideos: 1,
unwatchedVideos: {
$filter: {
input: "$allVideos",
as: "video",
cond: {
$not: {
$in: ["$$video._id", "$watchedVideos.videoId"],
},
},
},
},
},
},
]);
console.log(result);
return result;
}
}
export function CreateLearningProgressRepo(): LearningProgressRepo {
return new LearningProgressRepo();
}