From 73c7e55bf3ee743530b745ba1b10028a82e18894 Mon Sep 17 00:00:00 2001 From: morteza-mortezai Date: Sun, 21 Jun 2026 16:54:42 +0330 Subject: [PATCH] all food endponit --- .../foods/controllers/food.controller.ts | 13 ++++++ .../foods/dto/public-food-search-item.dto.ts | 42 +++++++++++++++++++ src/modules/foods/dto/search-foods.dto.ts | 25 +++++++++++ src/modules/foods/interface/food.interface.ts | 15 +++++++ src/modules/foods/providers/food.service.ts | 35 ++++++++++++++-- .../foods/repositories/food.repository.ts | 30 +++++++++++++ .../dto/active-restaurant-list-item.dto.ts | 3 -- .../providers/restaurants.service.ts | 3 +- .../repositories/rest.repository.ts | 4 +- 9 files changed, 159 insertions(+), 11 deletions(-) create mode 100644 src/modules/foods/dto/public-food-search-item.dto.ts create mode 100644 src/modules/foods/dto/search-foods.dto.ts diff --git a/src/modules/foods/controllers/food.controller.ts b/src/modules/foods/controllers/food.controller.ts index 3f127ed..3300d35 100644 --- a/src/modules/foods/controllers/food.controller.ts +++ b/src/modules/foods/controllers/food.controller.ts @@ -11,7 +11,10 @@ import { ApiParam, ApiBearerAuth, ApiHeader, + ApiOkResponse, } from '@nestjs/swagger'; +import { SearchFoodsDto } from '../dto/search-foods.dto'; +import { PublicFoodSearchItemDto } from '../dto/public-food-search-item.dto'; import { AdminAuthGuard } from 'src/modules/auth/guards/adminAuth.guard'; import { RestId, UserId } from 'src/common/decorators'; import { AuthGuard } from 'src/modules/auth/guards/auth.guard'; @@ -37,6 +40,16 @@ export class FoodController { return this.foodsService.findByRestuarantSlug(slug); } + @Get('public/foods/search') + @ApiOperation({ summary: 'Search foods across active restaurants' }) + @ApiQuery({ name: 'search', required: true, type: String, description: 'Search term (min 2 characters)' }) + @ApiQuery({ name: 'page', required: false, type: Number }) + @ApiQuery({ name: 'limit', required: false, type: Number }) + @ApiOkResponse({ description: 'Paginated food search results', type: [PublicFoodSearchItemDto] }) + searchFoodsAcrossRestaurants(@Query() dto: SearchFoodsDto) { + return this.foodsService.searchAcrossRestaurants(dto); + } + @Get('public/foods/:foodId') @UseGuards(OptionalAuthGuard) @ApiHeader(API_HEADER_SLUG) diff --git a/src/modules/foods/dto/public-food-search-item.dto.ts b/src/modules/foods/dto/public-food-search-item.dto.ts new file mode 100644 index 0000000..7310684 --- /dev/null +++ b/src/modules/foods/dto/public-food-search-item.dto.ts @@ -0,0 +1,42 @@ +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; + +class FoodSearchRestaurantDto { + @ApiProperty({ example: 'zhivan', description: 'Restaurant slug' }) + slug!: string; + + @ApiProperty({ example: 'کافه ژیوان', description: 'Restaurant name' }) + name!: string; + + @ApiPropertyOptional({ example: 'https://example.com/logo.png', description: 'Restaurant logo URL' }) + logo?: string; +} + +export class PublicFoodSearchItemDto { + @ApiProperty({ example: '550e8400-e29b-41d4-a716-446655440000', description: 'Food ID' }) + id!: string; + + @ApiPropertyOptional({ example: 'پیتزا مخصوص', description: 'Food title' }) + title?: string; + + @ApiPropertyOptional({ example: 'پیتزا با پنیر و قارچ', description: 'Food description' }) + desc?: string; + + @ApiPropertyOptional({ example: 250000, description: 'Food price' }) + price?: number; + + @ApiPropertyOptional({ + example: ['https://example.com/food.jpg'], + description: 'Food image URLs', + type: [String], + }) + images?: string[]; + + @ApiProperty({ example: 0, description: 'Discount percentage' }) + discount!: number; + + @ApiProperty({ example: false, description: 'Whether the food is a special offer' }) + isSpecialOffer!: boolean; + + @ApiProperty({ type: FoodSearchRestaurantDto, description: 'Restaurant that serves this food' }) + restaurant!: FoodSearchRestaurantDto; +} diff --git a/src/modules/foods/dto/search-foods.dto.ts b/src/modules/foods/dto/search-foods.dto.ts new file mode 100644 index 0000000..e82b894 --- /dev/null +++ b/src/modules/foods/dto/search-foods.dto.ts @@ -0,0 +1,25 @@ +import { IsNotEmpty, IsNumber, IsOptional, IsString, Min, MinLength } from 'class-validator'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; + +export class SearchFoodsDto { + @ApiProperty({ example: 'پیتزا', description: 'Search term matched against food title and description' }) + @IsString() + @IsNotEmpty() + @MinLength(2) + search!: string; + + @ApiPropertyOptional({ default: 1, minimum: 1 }) + @IsOptional() + @Type(() => Number) + @IsNumber() + @Min(1) + page: number = 1; + + @ApiPropertyOptional({ default: 10, minimum: 1 }) + @IsOptional() + @Type(() => Number) + @IsNumber() + @Min(1) + limit: number = 10; +} diff --git a/src/modules/foods/interface/food.interface.ts b/src/modules/foods/interface/food.interface.ts index c7e1063..bc193e6 100644 --- a/src/modules/foods/interface/food.interface.ts +++ b/src/modules/foods/interface/food.interface.ts @@ -27,3 +27,18 @@ export interface PublicFoodListItem { isAvailable: boolean; unavailabilityMessage: string | null; } + +export interface PublicFoodSearchItem { + id: string; + title?: string; + desc?: string; + price?: number; + images?: string[]; + discount: number; + isSpecialOffer: boolean; + restaurant: { + slug: string; + name: string; + logo?: string; + }; +} diff --git a/src/modules/foods/providers/food.service.ts b/src/modules/foods/providers/food.service.ts index 8a74a4d..938fe7a 100644 --- a/src/modules/foods/providers/food.service.ts +++ b/src/modules/foods/providers/food.service.ts @@ -8,7 +8,9 @@ import { RequiredEntityData, FilterQuery } from '@mikro-orm/core'; import { Food } from '../entities/food.entity'; import { CategoryMessage, FoodMessage, RestMessage, CartMessage } from 'src/common/enums/message.enum'; import { Favorite } from '../entities/favorite.entity'; -import { MealType, PublicFoodListItem, FoodAvailabilityInfo } from '../interface/food.interface'; +import { MealType, PublicFoodListItem, PublicFoodSearchItem, FoodAvailabilityInfo } from '../interface/food.interface'; +import { SearchFoodsDto } from '../dto/search-foods.dto'; +import { PaginatedResult } from 'src/common/interfaces/pagination.interface'; import { Inventory } from 'src/modules/inventory/entities/inventory.entity'; import { RestaurantsService } from 'src/modules/restaurants/providers/restaurants.service'; import { CacheService } from 'src/modules/utils/cache.service'; @@ -86,6 +88,31 @@ export class FoodService { return this.foodRepository.findAllPaginated(restId, dto); } + async searchAcrossRestaurants(dto: SearchFoodsDto): Promise> { + const result = await this.foodRepository.searchActiveAcrossRestaurants(dto.search, { + page: dto.page, + limit: dto.limit, + }); + + return { + data: result.data.map((food): PublicFoodSearchItem => ({ + id: food.id, + title: food.title, + desc: food.desc, + price: food.price, + images: food.images, + discount: food.discount, + isSpecialOffer: food.isSpecialOffer, + restaurant: { + slug: food.restaurant.slug, + name: food.restaurant.name, + logo: food.restaurant.logo, + }, + })), + meta: result.meta, + }; + } + /** * Public food detail (only active foods are visible). */ @@ -198,7 +225,7 @@ export class FoodService { return { isAvailable: false, - unavailabilityMessage: `فقط در روزهای ${allowedDays} در دسترس است. امروز ${currentDayName} است.`, + unavailabilityMessage: `عدم تطابق روز`, }; } @@ -206,7 +233,7 @@ export class FoodService { if (mealType === null) { return { isAvailable: false, - unavailabilityMessage: 'فقط در ساعات وعده‌های غذایی (صبحانه، ناهار، عصرانه یا شام) در دسترس است.', + unavailabilityMessage: 'عدم تطابق ساعت', }; } @@ -216,7 +243,7 @@ export class FoodService { return { isAvailable: false, - unavailabilityMessage: `فقط برای ${allowedMealTypes} در دسترس است. زمان فعلی ${currentMealType} است.`, + unavailabilityMessage: `عدم تطابق ساعت`, }; } } diff --git a/src/modules/foods/repositories/food.repository.ts b/src/modules/foods/repositories/food.repository.ts index 3957061..29a769b 100644 --- a/src/modules/foods/repositories/food.repository.ts +++ b/src/modules/foods/repositories/food.repository.ts @@ -62,7 +62,37 @@ export class FoodRepository extends EntityRepository { totalPages, }, }; + } + async searchActiveAcrossRestaurants( + search: string, + opts: Pick = {}, + ): Promise> { + const { page = 1, limit = 10 } = opts; + const offset = (page - 1) * limit; + const pattern = `%${search}%`; + const where: FilterQuery = { + isActive: true, + restaurant: { isActive: true }, + $or: [{ title: { $ilike: pattern } }, { desc: { $ilike: pattern } }], + }; + + const [data, total] = await this.findAndCount(where, { + limit, + offset, + orderBy: { title: 'asc' }, + populate: ['restaurant'], + }); + + return { + data, + meta: { + total, + page, + limit, + totalPages: Math.ceil(total / limit), + }, + }; } } diff --git a/src/modules/restaurants/dto/active-restaurant-list-item.dto.ts b/src/modules/restaurants/dto/active-restaurant-list-item.dto.ts index 0c3d9f0..62f6afb 100644 --- a/src/modules/restaurants/dto/active-restaurant-list-item.dto.ts +++ b/src/modules/restaurants/dto/active-restaurant-list-item.dto.ts @@ -7,9 +7,6 @@ export class ActiveRestaurantListItemDto { @ApiProperty({ example: 'کافه ژیوان', description: 'Restaurant name' }) name!: string; - @ApiPropertyOptional({ example: 'بهترین کافه تهران', description: 'SEO title' }) - title?: string; - @ApiPropertyOptional({ example: 'تهران، خیابان ولیعصر پلاک ۱۲۳', description: 'Address' }) address?: string; diff --git a/src/modules/restaurants/providers/restaurants.service.ts b/src/modules/restaurants/providers/restaurants.service.ts index 2a653b4..f4a7860 100644 --- a/src/modules/restaurants/providers/restaurants.service.ts +++ b/src/modules/restaurants/providers/restaurants.service.ts @@ -163,10 +163,9 @@ export class RestaurantsService { async findAllActiveRestaurants(): Promise { const restaurants = await this.restRepository.findAllActive(); - return restaurants.map(({ slug, name, seoTitle, address, score, logo, establishedYear }) => ({ + return restaurants.map(({ slug, name, address, logo, establishedYear }) => ({ slug, name, - title: seoTitle, address, logo, establishedYear, diff --git a/src/modules/restaurants/repositories/rest.repository.ts b/src/modules/restaurants/repositories/rest.repository.ts index ab7134e..3885b75 100644 --- a/src/modules/restaurants/repositories/rest.repository.ts +++ b/src/modules/restaurants/repositories/rest.repository.ts @@ -81,8 +81,8 @@ export class RestRepository extends EntityRepository { return this.find( { isActive: true }, { - fields: ['slug', 'name', 'seoTitle', 'address', 'logo', 'establishedYear'], - orderBy: { name: 'asc' }, + fields: ['slug', 'name', 'address', 'logo', 'establishedYear'], + orderBy: { createdAt: 'asc' }, }, ); }