From 60c0bfb8d2e2354255d43bff60e513f5fd6194ad Mon Sep 17 00:00:00 2001 From: morteza-mortezai Date: Thu, 18 Dec 2025 12:39:25 +0330 Subject: [PATCH] favorite --- .../foods/controllers/food.controller.ts | 58 +++++++++++++++++-- src/modules/foods/entities/favorite.entity.ts | 14 +++++ src/modules/foods/entities/food.entity.ts | 4 ++ src/modules/foods/food.module.ts | 5 +- src/modules/foods/providers/food.service.ts | 32 +++++++++- src/modules/orders/crone/order.crone.ts | 6 ++ 6 files changed, 108 insertions(+), 11 deletions(-) create mode 100644 src/modules/foods/entities/favorite.entity.ts create mode 100644 src/modules/orders/crone/order.crone.ts diff --git a/src/modules/foods/controllers/food.controller.ts b/src/modules/foods/controllers/food.controller.ts index aa7f21a..d49d2b0 100644 --- a/src/modules/foods/controllers/food.controller.ts +++ b/src/modules/foods/controllers/food.controller.ts @@ -16,7 +16,9 @@ import { ApiHeader, } from '@nestjs/swagger'; import { AdminAuthGuard } from 'src/modules/auth/guards/adminAuth.guard'; -import { RestId } from 'src/common/decorators'; +import { RestId, UserId } from 'src/common/decorators'; +import { AuthGuard } from 'src/modules/auth/guards/auth.guard'; +import { OptionalAuthGuard } from 'src/modules/auth/guards/optinalAuth.guard'; @ApiTags('foods') @Controller() @@ -39,18 +41,62 @@ export class FoodController { return this.foodsService.findAllByRestaurant(slug); } + @Get('public/foods/:foodId') + @UseGuards(OptionalAuthGuard) + @ApiHeader({ + name: 'X-Slug', + required: true, + schema: { + type: 'string', + default: 'zhivan', + }, + }) + @ApiBearerAuth() @ApiOperation({ summary: 'Get a food by id' }) @ApiParam({ name: 'foodId', required: true }) - @ApiOkResponse({ description: 'The food', type: CreateFoodDto }) - @ApiNotFoundResponse({ description: 'Food not found' }) - findPublicFoodById(@Param('foodId') foodId: string) { - return this.foodsService.findPublicById(foodId); + findPublicFoodById(@Param('foodId') foodId: string, @UserId() userId?: string):Promise { + const a= this.foodsService.findPublicById(foodId, userId); + return a + } + + @Post('public/foods/favorite/:foodId') + @UseGuards(AuthGuard) + @ApiHeader({ + name: 'X-Slug', + required: true, + schema: { + type: 'string', + default: 'zhivan', + }, + }) + @ApiBearerAuth() + @ApiOperation({ summary: 'Set a food as favorite' }) + @ApiParam({ name: 'foodId', required: true }) + setAsFavorute(@Param('foodId') foodId: string, @UserId() userId: string) { + return this.foodsService.addFavorite(userId, foodId); + } + + @Delete('public/foods/favorite/:foodId') + @UseGuards(AuthGuard) + @ApiHeader({ + name: 'X-Slug', + required: true, + schema: { + type: 'string', + default: 'zhivan', + }, + }) + @ApiBearerAuth() + @ApiOperation({ summary: 'Remove a food favorite' }) + @ApiParam({ name: 'foodId', required: true }) + removeFavorite(@Param('foodId') foodId: string, @UserId() userId: string) { + return this.foodsService.removeFavorite(userId, foodId); } @UseGuards(AdminAuthGuard) @ApiBearerAuth() - @Post('admin/foods') + @Post('admin/foods') @ApiOperation({ summary: 'Create a new food' }) @ApiCreatedResponse({ description: 'The food has been successfully created.', type: CreateFoodDto }) @ApiBody({ type: CreateFoodDto }) diff --git a/src/modules/foods/entities/favorite.entity.ts b/src/modules/foods/entities/favorite.entity.ts new file mode 100644 index 0000000..245e102 --- /dev/null +++ b/src/modules/foods/entities/favorite.entity.ts @@ -0,0 +1,14 @@ +import { Entity, ManyToOne, Unique } from '@mikro-orm/core'; +import { BaseEntity } from '../../../common/entities/base.entity'; +import { User } from '../../users/entities/user.entity'; +import { Food } from '../../foods/entities/food.entity'; + +@Entity({ tableName: 'favorites' }) +@Unique({ properties: ['user', 'food'] }) +export class Favorite extends BaseEntity { + @ManyToOne(() => User) + user: User; + + @ManyToOne(() => Food) + food: Food; +} diff --git a/src/modules/foods/entities/food.entity.ts b/src/modules/foods/entities/food.entity.ts index 8ef6028..14443ae 100644 --- a/src/modules/foods/entities/food.entity.ts +++ b/src/modules/foods/entities/food.entity.ts @@ -5,6 +5,7 @@ import { Restaurant } from '../../../modules/restaurants/entities/restaurant.ent import { Review } from 'src/modules/review/entities/review.entity'; import { Inventory } from 'src/modules/inventory/entities/inventory.entity'; import { MealType } from '../interface/food.interface'; +import { Favorite } from './favorite.entity'; @Entity({ tableName: 'foods' }) @Index({ properties: ['restaurant', 'isActive'] }) @@ -26,6 +27,9 @@ export class Food extends BaseEntity { }) inventory?: Inventory; + @OneToMany(() => Favorite, favorite => favorite.food) + favorites = new Collection(this); + @Property({ nullable: true }) title?: string; diff --git a/src/modules/foods/food.module.ts b/src/modules/foods/food.module.ts index fda3599..ee9a16c 100644 --- a/src/modules/foods/food.module.ts +++ b/src/modules/foods/food.module.ts @@ -12,11 +12,12 @@ import { RestaurantsModule } from '../restaurants/restaurants.module'; import { AuthModule } from '../auth/auth.module'; import { JwtModule } from '@nestjs/jwt'; import { UtilsModule } from '../utils/utils.module'; +import { Favorite } from './entities/favorite.entity'; @Module({ - imports: [MikroOrmModule.forFeature([Food, Category]), RestaurantsModule, AuthModule, JwtModule, UtilsModule], + imports: [MikroOrmModule.forFeature([Food, Category, Favorite]), RestaurantsModule, AuthModule, JwtModule, UtilsModule], controllers: [FoodController, CategoryController], providers: [FoodService, CategoryService, FoodRepository, CategoryRepository], exports: [FoodRepository, CategoryRepository], }) -export class FoodModule {} +export class FoodModule { } diff --git a/src/modules/foods/providers/food.service.ts b/src/modules/foods/providers/food.service.ts index c2b78d1..1d1f5d8 100644 --- a/src/modules/foods/providers/food.service.ts +++ b/src/modules/foods/providers/food.service.ts @@ -9,6 +9,7 @@ import { Food } from '../entities/food.entity'; import { CategoryMessage, FoodMessage, RestMessage } from 'src/common/enums/message.enum'; import { RestRepository } from 'src/modules/restaurants/repositories/rest.repository'; import { CacheService } from '../../utils/cache.service'; +import { Favorite } from '../entities/favorite.entity'; @Injectable() export class FoodService { @@ -76,10 +77,17 @@ export class FoodService { /** * Public food detail (only active foods are visible). */ - async findPublicById(id: string): Promise { - const food = await this.foodRepository.findOne({ id, isActive: true }, { populate: ['category'] }); + async findPublicById(foodId: string, userId?: string):Promise { + const food = await this.foodRepository.findOne({ id: foodId, isActive: true }, { populate: ['category'] }); if (!food) throw new NotFoundException(FoodMessage.NOT_FOUND); - return food; + let isFavorite = false + if (userId) { + isFavorite = await this.em.count(Favorite, { user: { id: userId }, food: { id: foodId } }) > 0 + } + return { + ...food, + isFavorite, + };; } /** @@ -87,6 +95,7 @@ export class FoodService { */ async findAdminById(restId: string, id: string): Promise { const food = await this.foodRepository.findOne({ id, restaurant: { id: restId } }, { populate: ['category'] }); + if (!food) throw new NotFoundException(FoodMessage.NOT_FOUND); return food; } @@ -164,6 +173,23 @@ export class FoodService { await this.invalidateRestaurantFoodsCache(restaurantSlug); } + async removeFavorite(userId: string, foodId: string) { + return this.em.nativeDelete(Favorite, { + user: userId, + food: foodId, + }); + } + + async addFavorite(userId: string, foodId: string) { + const favorite = this.em.create(Favorite, { + user: userId, + food: foodId, + }); + + await this.em.persistAndFlush(favorite); + } + + /** * Invalidate cache for restaurant foods */ diff --git a/src/modules/orders/crone/order.crone.ts b/src/modules/orders/crone/order.crone.ts new file mode 100644 index 0000000..7886d6e --- /dev/null +++ b/src/modules/orders/crone/order.crone.ts @@ -0,0 +1,6 @@ +import { Injectable } from "@nestjs/common"; + +@Injectable() +export class OrdersCrone { + +} \ No newline at end of file