diff --git a/.vscode/settings.json b/.vscode/settings.json index 773cd7b..28ec25e 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -7,6 +7,6 @@ "source.fixAll.eslint": "explicit" }, "[typescript]": { - "editor.defaultFormatter": "esbenp.prettier-vscode" + "editor.defaultFormatter": "vscode.typescript-language-features" } } diff --git a/src/modules/restaurants/controllers/restaurants.controller.ts b/src/modules/restaurants/controllers/restaurants.controller.ts index bcd3e6a..105e51d 100644 --- a/src/modules/restaurants/controllers/restaurants.controller.ts +++ b/src/modules/restaurants/controllers/restaurants.controller.ts @@ -1,7 +1,8 @@ -import { Controller, Get, Post, Body, Patch, Param, UseGuards, Delete } from '@nestjs/common'; +import { Controller, Get, Post, Body, Patch, Param, UseGuards, Delete, Query } from '@nestjs/common'; import { RestaurantsService } from '../providers/restaurants.service'; import { CreateRestaurantDto } from '../dto/create-restaurant.dto'; import { UpdateRestaurantDto } from '../dto/update-restaurant.dto'; +import { FindRestaurantsDto } from '../dto/find-restaurants.dto'; import { AdminAuthGuard } from 'src/modules/auth/guards/adminAuth.guard'; import { ApiBearerAuth, @@ -13,28 +14,24 @@ import { ApiHeader, } from '@nestjs/swagger'; import { RestId } from 'src/common/decorators'; +import { API_HEADER_SLUG } from 'src/common/constants'; +import { SuperAdminAuthGuard } from 'src/modules/auth/guards/superAdminAuth.guard'; @ApiTags('restaurants') @Controller() export class RestaurantsController { - constructor(private readonly restaurantsService: RestaurantsService) {} + constructor(private readonly restaurantsService: RestaurantsService) { } @Get('public/restaurants/:slug') @ApiOperation({ summary: 'Get restaurant specification by slug' }) - @ApiHeader({ - name: 'X-Slug', - required: true, - schema: { - type: 'string', - default: 'zhivan', - }, - }) + @ApiHeader(API_HEADER_SLUG) @ApiParam({ name: 'slug', required: true, description: 'Restaurant slug' }) @ApiNotFoundResponse({ description: 'Restaurant not found' }) findBySlug(@Param('slug') slug: string) { return this.restaurantsService.getRestaurantSpecification(slug); } + /** Admin Endpoints */ @UseGuards(AdminAuthGuard) @ApiBearerAuth() @Post('admin/restaurants') @@ -44,13 +41,6 @@ export class RestaurantsController { return this.restaurantsService.create(createRestaurantDto); } - // @Get('admin/restaurants') - // @UseGuards(AdminAuthGuard) - // @ApiBearerAuth() - // @ApiOperation({ summary: 'Get all restaurants' }) - // findAll() { - // return this.restaurantsService.findAll(); - // } @UseGuards(AdminAuthGuard) @ApiBearerAuth() @@ -86,4 +76,13 @@ export class RestaurantsController { remove(@Param('id') id: string) { return this.restaurantsService.remove(id); } + + /** Super Admin Endpoints */ + // @UseGuards(SuperAdminAuthGuard) + @ApiBearerAuth() + @ApiOperation({ summary: 'Get all restaurants with pagination and filters' }) + @Get('super-admin/restaurants') + findAll(@Query() dto: FindRestaurantsDto) { + return this.restaurantsService.findAll(dto); + } } diff --git a/src/modules/restaurants/dto/find-restaurants.dto.ts b/src/modules/restaurants/dto/find-restaurants.dto.ts new file mode 100644 index 0000000..1840dff --- /dev/null +++ b/src/modules/restaurants/dto/find-restaurants.dto.ts @@ -0,0 +1,73 @@ +import { + IsOptional, + IsString, + IsNumber, + Min, + IsIn, + IsEnum, + IsBoolean, +} from 'class-validator'; +import { Type, Transform } from 'class-transformer'; +import { ApiPropertyOptional } from '@nestjs/swagger'; +import { PlanEnum } from '../interface/plan.interface'; + +const sortOrderOptions = ['asc', 'desc'] as const; +type SortOrder = (typeof sortOrderOptions)[number]; + +export class FindRestaurantsDto { + @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; + + @ApiPropertyOptional({ + description: 'Search by name, slug, domain, or address', + }) + @IsOptional() + @IsString() + search?: string; + + @ApiPropertyOptional({ + description: 'Filter by active status', + type: Boolean, + }) + @IsOptional() + @Transform(({ value }) => { + if (value === 'true') return true; + if (value === 'false') return false; + return value; + }) + @IsBoolean() + isActive?: boolean; + + @ApiPropertyOptional({ + enum: PlanEnum, + description: 'Filter by plan type', + }) + @IsOptional() + @IsEnum(PlanEnum) + plan?: PlanEnum; + + @ApiPropertyOptional({ default: 'createdAt' }) + @IsOptional() + @IsString() + orderBy: string = 'createdAt'; + + @ApiPropertyOptional({ + enum: sortOrderOptions, + default: 'desc', + }) + @IsOptional() + @IsIn(sortOrderOptions) + order: SortOrder = 'desc'; +} + diff --git a/src/modules/restaurants/providers/restaurants.service.ts b/src/modules/restaurants/providers/restaurants.service.ts index c803379..5787229 100644 --- a/src/modules/restaurants/providers/restaurants.service.ts +++ b/src/modules/restaurants/providers/restaurants.service.ts @@ -5,13 +5,15 @@ import { Restaurant } from '../entities/restaurant.entity'; import { EntityManager } from '@mikro-orm/postgresql'; import { RestRepository } from '../repositories/rest.repository'; import { RestMessage } from 'src/common/enums/message.enum'; +import { FindRestaurantsDto } from '../dto/find-restaurants.dto'; +import { PaginatedResult } from 'src/common/interfaces/pagination.interface'; @Injectable() export class RestaurantsService { constructor( private readonly em: EntityManager, private readonly restRepository: RestRepository, - ) {} + ) { } async create(dto: CreateRestaurantDto): Promise { const restaurant = this.em.create(Restaurant, { @@ -24,8 +26,16 @@ export class RestaurantsService { return restaurant; } - findAll() { - return this.restRepository.findAll(); + async findAll(dto: FindRestaurantsDto): Promise> { + return this.restRepository.findAllPaginated({ + page: dto.page, + limit: dto.limit, + search: dto.search, + isActive: dto.isActive, + plan: dto.plan, + orderBy: dto.orderBy, + order: dto.order, + }); } async findBySlug(slug: string): Promise { diff --git a/src/modules/restaurants/repositories/rest.repository.ts b/src/modules/restaurants/repositories/rest.repository.ts index 8e53dd6..7eb58db 100644 --- a/src/modules/restaurants/repositories/rest.repository.ts +++ b/src/modules/restaurants/repositories/rest.repository.ts @@ -1,10 +1,79 @@ import { EntityManager, EntityRepository } from '@mikro-orm/postgresql'; +import { FilterQuery } from '@mikro-orm/core'; import { Restaurant } from '../entities/restaurant.entity'; import { Injectable } from '@nestjs/common'; +import { PaginatedResult } from 'src/common/interfaces/pagination.interface'; +import { PlanEnum } from '../interface/plan.interface'; + +type FindRestaurantsOpts = { + page?: number; + limit?: number; + search?: string; + isActive?: boolean; + plan?: PlanEnum; + orderBy?: string; + order?: 'asc' | 'desc'; +}; @Injectable() export class RestRepository extends EntityRepository { constructor(readonly em: EntityManager) { super(em, Restaurant); } + + /** + * Find restaurants with pagination and optional filters. + * Supports: search (name/slug/domain/address), isActive, plan, ordering. + */ + async findAllPaginated(opts: FindRestaurantsOpts = {}): Promise> { + const { + page = 1, + limit = 10, + search, + isActive, + plan, + orderBy = 'createdAt', + order = 'desc', + } = opts; + + const offset = (page - 1) * limit; + + const where: FilterQuery = {}; + + if (typeof isActive === 'boolean') { + where.isActive = isActive; + } + + if (plan) { + where.plan = plan; + } + + if (search) { + const pattern = `%${search}%`; + where.$or = [ + { name: { $ilike: pattern } }, + { slug: { $ilike: pattern } }, + { domain: { $ilike: pattern } }, + { address: { $ilike: pattern } }, + ]; + } + + const [data, total] = await this.findAndCount(where, { + limit, + offset, + orderBy: { [orderBy]: order.toLowerCase() as 'asc' | 'desc' }, + }); + + const totalPages = Math.ceil(total / limit); + + return { + data, + meta: { + total, + page, + limit, + totalPages, + }, + }; + } }