This commit is contained in:
2026-01-14 21:00:43 +03:30
parent d221599238
commit 264b6585a8
10 changed files with 257 additions and 27 deletions
+1
View File
@@ -127,6 +127,7 @@ export const enum CategoryMessage {
NOT_CREATED = 'ایجاد دسته‌بندی با خطا مواجه شد',
NOT_UPDATED = 'به‌روزرسانی دسته‌بندی با خطا مواجه شد',
NOT_DELETED = 'حذف دسته‌بندی با خطا مواجه شد',
ALREADY_EXIST = 'وجود دارد',
}
export const enum UserMessage {
USER_NOT_FOUND = 'کاربری با این مشخصات یافت نشد',
@@ -1,5 +1,5 @@
import { Controller, Get, Post, Body, Patch, Param, Delete, Query, UseGuards } from '@nestjs/common';
import { productService } from '../providers/product.service';
import { ProductService } from '../providers/product.service';
import { CreateproductDto } from '../dto/create-product.dto';
import { UpdateproductDto } from '../dto/update-product.dto';
import { FindproductsDto } from '../dto/find-products.dto';
@@ -17,11 +17,17 @@ import { UserId } from 'src/common/decorators';
import { AuthGuard } from 'src/modules/auth/guards/auth.guard';
import { Permission } from 'src/common/enums/permission.enum';
import { Permissions } from 'src/common/decorators/permissions.decorator';
import { CreateCategoryDto } from '../dto/create-category.dto';
import { CategoryService } from '../providers/category.service';
import { FindCategoriesDto } from '../dto/find-categories.dto';
@ApiTags('products')
@Controller()
export class productController {
constructor(private readonly productsService: productService) { }
constructor(
private readonly productService: ProductService,
private readonly categoryService: CategoryService,
) { }
// @Get('public/products/:productId')
@@ -43,7 +49,7 @@ export class productController {
@ApiOperation({ summary: 'Create a new product' })
@ApiBody({ type: CreateproductDto })
create(@Body() createproductDto: CreateproductDto) {
return this.productsService.create(createproductDto);
return this.productService.create(createproductDto);
}
@Get('admin/products')
@@ -59,7 +65,7 @@ export class productController {
@ApiQuery({ name: 'categoryId', required: false, type: String })
@ApiQuery({ name: 'isActive', required: false, type: Boolean })
async findAll(@Query() dto: FindproductsDto) {
const result = await this.productsService.findAll(dto);
const result = await this.productService.findAll(dto);
return result;
}
@@ -70,7 +76,7 @@ export class productController {
@ApiOperation({ summary: 'Get a product by id' })
@ApiParam({ name: 'id', required: true })
findById(@Param('id') id: string,) {
return this.productsService.findById(id);
return this.productService.findById(id);
}
@Patch('admin/products/:id')
@@ -81,7 +87,7 @@ export class productController {
@ApiParam({ name: 'id', required: true })
@ApiBody({ type: UpdateproductDto })
update(@Param('id') id: string, @Body() updateproductDto: UpdateproductDto) {
return this.productsService.update(id, updateproductDto);
return this.productService.update(id, updateproductDto);
}
@Delete('admin/products/:id')
@@ -91,6 +97,33 @@ export class productController {
@ApiOperation({ summary: 'Delete (soft) a product' })
@ApiParam({ name: 'id', required: true })
remove(@Param('id') id: string,) {
return this.productsService.remove(id);
return this.productService.remove(id);
}
/**CATEGORY */
@Post('admin/product/category')
// @UseGuards(AdminAuthGuard)
@ApiBearerAuth()
@Permissions(Permission.MANAGE_productS)
@ApiOperation({ summary: 'Create a new category' })
@ApiBody({ type: CreateCategoryDto })
createCategory(@Body() createproductDto: CreateCategoryDto) {
return this.categoryService.create(createproductDto);
}
@Get('admin/products/category')
// @UseGuards(AdminAuthGuard)
@ApiBearerAuth()
@Permissions(Permission.MANAGE_productS)
@ApiOperation({ summary: 'Get a paginated list of Categories' })
@ApiQuery({ name: 'page', required: false, type: Number })
@ApiQuery({ name: 'limit', required: false, type: Number })
@ApiQuery({ name: 'search', required: false, type: String })
@ApiQuery({ name: 'orderBy', required: false, type: String })
@ApiQuery({ name: 'order', required: false, enum: ['asc', 'desc'] })
@ApiQuery({ name: 'isActive', required: false, type: Boolean })
async findAllCategories(@Query() dto: FindCategoriesDto) {
const result = await this.categoryService.findAll(dto);
return result;
}
}
@@ -1,11 +1,16 @@
import { IsString, IsOptional, IsBoolean, IsInt, Min } from 'class-validator';
import { ApiPropertyOptional } from '@nestjs/swagger';
import { ApiPropertyOptional,ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer';
export class CreateCategoryDto {
@IsString()
@ApiPropertyOptional({ example: 'ایرانی' })
title!: string;
@ApiProperty({ example: 'چاپ' })
title: string;
@IsOptional()
@IsInt()
@ApiPropertyOptional()
parentId?: number;
@IsOptional()
@IsBoolean()
@@ -12,9 +12,9 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
export class CreateproductDto {
@IsNotEmpty()
@IsString()
@IsInt()
@ApiProperty()
categoryId: string;
categoryId: number;
@IsOptional()
@IsString()
@@ -0,0 +1,39 @@
import { IsOptional, IsNumber, IsString, IsIn, IsBoolean } from 'class-validator';
import { ApiPropertyOptional } from '@nestjs/swagger';
import { Type } from 'class-transformer';
export class FindCategoriesDto {
@IsOptional()
@IsNumber()
@Type(() => Number)
@ApiPropertyOptional({ example: 1 })
page?: number;
@IsOptional()
@IsNumber()
@Type(() => Number)
@ApiPropertyOptional({ example: 10 })
limit?: number;
@IsOptional()
@IsString()
@ApiPropertyOptional()
search?: string;
@IsOptional()
@IsString()
@ApiPropertyOptional({ example: 'createdAt' })
orderBy?: string;
@IsOptional()
@IsIn(['asc', 'desc'])
@ApiPropertyOptional({ example: 'desc' })
order?: 'asc' | 'desc';
@IsOptional()
@IsBoolean()
@Type(() => Boolean)
@ApiPropertyOptional({ example: true })
isActive?: boolean;
}
+12 -10
View File
@@ -1,23 +1,25 @@
import { Entity, Index, Property, Collection, OneToMany, ManyToOne } from '@mikro-orm/core';
import { Product } from './product.entity';
import { Entity, Index, Property, Collection, OneToMany, ManyToOne, PrimaryKey } from '@mikro-orm/core';
// import { Product } from './product.entity';
import { BaseEntity } from '../../../common/entities/base.entity';
@Entity({ tableName: 'categories' })
@Index({ properties: ['isActive'] })
export class Category extends BaseEntity {
@ManyToOne(() => Category)
@ManyToOne(() => Category, { nullable: true })
parent?: Category | null
@Property({ primary: true })
id: bigint
@OneToMany(() => Category, (c) => c.parent)
children?: Category[]
@Property()
title!: string;
@PrimaryKey({ type: 'bigint', autoincrement: true })
id: number
@OneToMany(() => Product, product => product.category)
Products = new Collection<Product>(this);
@Property({ unique: true })
title: string;
// @OneToMany(() => Product, product => product.category)
// Products = new Collection<Product>(this);
@Property({ default: true })
isActive: boolean = true;
+4 -3
View File
@@ -1,5 +1,5 @@
import { Module } from '@nestjs/common';
import { productService } from './providers/product.service';
import { ProductService } from './providers/product.service';
import { productController } from './controllers/product.controller';
import { ProductRepository } from './repositories/product.repository';
import { MikroOrmModule } from '@mikro-orm/nestjs';
@@ -11,6 +11,7 @@ import { AuthModule } from '../auth/auth.module';
import { JwtModule } from '@nestjs/jwt';
import { UtilsModule } from '../util/utils.module';
import { CategoryRepository } from './repositories/category.repository';
import { CategoryService } from './providers/category.service';
@Module({
imports: [
@@ -20,7 +21,7 @@ import { CategoryRepository } from './repositories/category.repository';
UtilsModule,
],
controllers: [productController,],
providers: [productService, ProductRepository,CategoryRepository],
exports: [ProductRepository,CategoryRepository],
providers: [ProductService, ProductRepository, CategoryRepository, CategoryService],
exports: [ProductRepository, CategoryRepository, CategoryService],
})
export class productModule { }
@@ -0,0 +1,143 @@
import { BadGatewayException, Injectable, NotFoundException } from '@nestjs/common';
import { CreateproductDto } from '../dto/create-product.dto';
import { FindproductsDto } from '../dto/find-products.dto';
import { ProductRepository } from '../repositories/product.repository';
import { EntityManager } from '@mikro-orm/postgresql';
import { RequiredEntityData } from '@mikro-orm/core';
import { Product } from '../entities/product.entity';
import { CategoryMessage, productMessage } from 'src/common/enums/message.enum';
import { CategoryRepository } from '../repositories/category.repository';
import { CreateCategoryDto } from '../dto/create-category.dto';
import { Category } from '../entities/category.entity';
import { FindCategoriesDto } from '../dto/find-categories.dto';
@Injectable()
export class CategoryService {
constructor(
private readonly productRepository: ProductRepository,
private readonly categoryRepository: CategoryRepository,
private readonly em: EntityManager,
) { }
async create(dto: CreateCategoryDto) {
let parent: null | Category = null
if (dto.parentId) {
parent = await this.categoryRepository.findOne({ id: dto.parentId })
if (!parent) {
throw new BadGatewayException('Parent category not found')
}
}
const data: RequiredEntityData<Category> = {
title: dto.title,
isActive: dto.isActive ?? true,
order: dto.order,
avatarUrl: dto.avatarUrl,
parent
};
const category = this.categoryRepository.create(data)
await this.em.persistAndFlush(category)
return category
}
// async update(productId: string, createproductDto: Partial<CreateproductDto>) {
// const { categoryId, ...rest } = createproductDto;
// const product = await this.productRepository.findOne({ id: productId })
// if (!product) {
// throw new NotFoundException(productMessage.NOT_FOUND);
// }
// if (categoryId) {
// const category = await this.categoryRepository.findOne({ id: categoryId });
// if (!category) {
// throw new NotFoundException(CategoryMessage.NOT_FOUND);
// }
// this.productRepository.assign(product, { category })
// }
// this.productRepository.assign(product, rest)
// //TODO : which one od these are correct
// // this.productRepository.nativeUpdate(productId,product)
// this.em.persistAndFlush(product)
// }
findAll(dto: FindCategoriesDto) {
return this.categoryRepository.findAllPaginated(dto);
}
async findById(productId: string): Promise<Product> {
const product = await this.productRepository.findOne({ id: productId }, { populate: ['category', 'attributes'] });
if (!product) throw new NotFoundException(productMessage.NOT_FOUND);
return product;
}
// async update( id: string, dto: Partial<CreateproductDto>): Promise<Product> {
// const { categoryId, dailyStock, ...rest } = dto;
// const product = await this.productRepository.findOne({ id, restaurant: { id: } }, { populate: ['restaurant'] });
// if (!product) {
// throw new NotFoundException(productMessage.NOT_FOUND);
// }
// // attach new categories if provided (adds, does not attempt to preserve/remove existing ones)
// if (categoryId) {
// const category = await this.categoryRepository.findOne({ id: categoryId, restaurant: { id: } });
// if (!category) {
// throw new NotFoundException(CategoryMessage.NOT_FOUND);
// }
// this.em.assign(product, { category: category });
// }
// // assign other fields from DTO (excluding dailyStock handled below)
// this.em.assign(product, rest);
// // Persist product and update/create related inventory atomically
// await this.em.transactional(async em => {
// await em.persistAndFlush(product);
// if (typeof dailyStock !== 'undefined') {
// const inventoryRecord = await em.findOne(Inventory, { product: product });
// if (inventoryRecord) {
// inventoryRecord.totalStock = dailyStock;
// } else {
// em.create(Inventory, {
// product: product,
// availableStock: dailyStock,
// totalStock: dailyStock,
// });
// }
// }
// await em.flush();
// });
// // Re-load the product to ensure populated relations and stable instance
// const savedproduct = await this.productRepository.findOne({ id: product.id }, { populate: ['category', 'restaurant'] });
// // Invalidate cache for the restaurant if present (optional)
// // await this.invalidateRestaurantproductsCache(savedproduct?.restaurant?.slug);
// return savedproduct ?? product;
// }
async remove(id: string): Promise<void> {
const product = await this.productRepository.findOne({ id });
if (!product) {
throw new NotFoundException(productMessage.NOT_FOUND);
}
product.deletedAt = new Date();
return await this.em.persistAndFlush(product);
}
}
@@ -9,7 +9,7 @@ import { CategoryMessage, productMessage } from 'src/common/enums/message.enum';
import { CategoryRepository } from '../repositories/category.repository';
@Injectable()
export class productService {
export class ProductService {
constructor(
private readonly productRepository: ProductRepository,
@@ -25,17 +25,23 @@ export class CategoryRepository extends EntityRepository<Category> {
const offset = (page - 1) * limit;
const where: FilterQuery<Category> = {};
const where: FilterQuery<Category> = { parent: null };
if (typeof isActive === 'boolean') {
where.isActive = isActive;
}
if (search) {
where.$or = [
]
}
const [data, total] = await this.findAndCount(where, {
limit,
offset,
orderBy: { [orderBy]: order.toLowerCase() as 'asc' | 'desc' },
populate: ['children']
});
const totalPages = Math.ceil(total / limit);