diff --git a/src/modules/product/controllers/product.controller.ts b/src/modules/product/controllers/product.controller.ts index f3a28d4..09a2605 100644 --- a/src/modules/product/controllers/product.controller.ts +++ b/src/modules/product/controllers/product.controller.ts @@ -56,6 +56,8 @@ export class ProductController { } + + @Get('admin/category/:id') @UseGuards(AdminAuthGuard) @ApiOperation({ summary: 'Get a category by id' }) @@ -89,9 +91,23 @@ export class ProductController { @UseGuards(AuthGuard) @ApiOperation({ summary: 'Get list of categories' }) async findAllCategoriesAsUser() { - return this.categoryService.findAll({ limit: 1000, isActive: true }); + const result = await this.categoryService.findAll({ limit: 1000, isActive: true }); + return result.data; } + @Get('public/category/tree') + @UseGuards(AuthGuard) + @ApiOperation({ summary: 'Get a tree list of Categories' }) + @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 findTreeCategories(@Query() dto: FindCategoriesDto) { + const result = await this.categoryService.findTree(dto); + return result; + } + + @Get('public/products') @UseGuards(AuthGuard) @ApiOperation({ summary: 'Get list of products' }) diff --git a/src/modules/product/providers/category.service.ts b/src/modules/product/providers/category.service.ts index a685d0e..6c24454 100644 --- a/src/modules/product/providers/category.service.ts +++ b/src/modules/product/providers/category.service.ts @@ -2,7 +2,7 @@ import { BadGatewayException, Injectable, NotFoundException } from '@nestjs/comm import { EntityManager } from '@mikro-orm/postgresql'; import { RequiredEntityData } from '@mikro-orm/core'; import { CategoryMessage, productMessage } from 'src/common/enums/message.enum'; -import { CategoryRepository } from '../repositories/category.repository'; +import { CategoryRepository, CategoryTreeNode } from '../repositories/category.repository'; import { CreateCategoryDto } from '../dto/create-category.dto'; import { Category } from '../entities/category.entity'; import { FindCategoriesDto } from '../dto/find-categories.dto'; @@ -70,10 +70,14 @@ export class CategoryService { } - findAll(dto: FindCategoriesDto) { + findAll(dto: FindCategoriesDto): Promise<{ data: Category[]; meta: { total: number; page: number; limit: number; totalPages: number } }> { return this.categoryRepository.findAllPaginated(dto); } + findTree(dto: FindCategoriesDto): Promise { + return this.categoryRepository.findTree(dto); + } + async findById(catId: string): Promise { const category = await this.categoryRepository.findOne({ id: catId }, { populate: ['children', 'parent'] }); diff --git a/src/modules/product/repositories/category.repository.ts b/src/modules/product/repositories/category.repository.ts index d33afb7..31bc806 100644 --- a/src/modules/product/repositories/category.repository.ts +++ b/src/modules/product/repositories/category.repository.ts @@ -1,7 +1,6 @@ import { Injectable } from '@nestjs/common'; import { EntityManager, EntityRepository } from '@mikro-orm/postgresql'; import { FilterQuery } from '@mikro-orm/core'; -import { Product } from '../entities/product.entity'; import { PaginatedResult } from 'src/common/interfaces/pagination.interface'; import { Category } from '../entities/category.entity'; @@ -14,31 +13,60 @@ type FindCategoriesOpts = { isActive?: boolean; }; +export type CategoryTreeNode = { + id: string; + createdAt: Date; + deletedAt?: Date; + parent: { id: string } | null; + title: string; + isActive: boolean; + avatarUrl?: string; + order?: number; + children: CategoryTreeNode[]; +}; + @Injectable() export class CategoryRepository extends EntityRepository { constructor(readonly em: EntityManager) { super(em, Category); } - private buildCategoryTree(categories: Category[]): Category[] { - const roots: Category[] = []; + private buildCategoryTree(categories: Category[]): CategoryTreeNode[] { + const roots: CategoryTreeNode[] = []; + const nodesById = new Map(); for (const category of categories) { - category.children.removeAll(); + const parentId = category.parent?.id ?? null; + nodesById.set(category.id, { + id: category.id, + createdAt: category.createdAt, + deletedAt: category.deletedAt, + parent: parentId ? { id: parentId } : null, + title: category.title, + isActive: category.isActive, + avatarUrl: category.avatarUrl, + order: category.order, + children: [], + }); } for (const category of categories) { - const parentId = category.parent?.id; + const node = nodesById.get(category.id); + if (!node) continue; - if (parentId) { - const parent = categories.find((item) => item.id === parentId); - if (parent) { - parent.children.add(category); - continue; - } + const parentId = category.parent?.id; + if (!parentId) { + roots.push(node); + continue; } - roots.push(category); + const parentNode = nodesById.get(parentId); + if (parentNode) { + parentNode.children.push(node); + continue; + } + + roots.push(node); } return roots; @@ -57,8 +85,8 @@ export class CategoryRepository extends EntityRepository { if (search) { where.$or = [ - - ] + { title: { $ilike: `%${search}%` } }, + ]; } const allCategories = await this.find(where, { @@ -66,9 +94,8 @@ export class CategoryRepository extends EntityRepository { populate: ['parent'], }); - const roots = this.buildCategoryTree(allCategories); - const data = roots.slice(offset, offset + limit); - const total = roots.length; + const total = allCategories.length; + const data = allCategories.slice(offset, offset + limit); const totalPages = Math.ceil(total / limit); return { @@ -81,4 +108,26 @@ export class CategoryRepository extends EntityRepository { }, }; } + + async findTree(opts: FindCategoriesOpts = {}): Promise { + const { search, orderBy = 'createdAt', order = 'desc', isActive } = opts; + const where: FilterQuery = {}; + + if (typeof isActive === 'boolean') { + where.isActive = isActive; + } + + if (search) { + where.$or = [ + { title: { $ilike: `%${search}%` } }, + ]; + } + + const allCategories = await this.find(where, { + orderBy: { [orderBy]: order.toLowerCase() as 'asc' | 'desc' }, + populate: ['parent'], + }); + + return this.buildCategoryTree(allCategories); + } }