diff --git a/src/catalog/catalog.controller.ts b/src/catalog/catalog.controller.ts index 8aa5996..0c7257d 100644 --- a/src/catalog/catalog.controller.ts +++ b/src/catalog/catalog.controller.ts @@ -1,11 +1,11 @@ import { Controller, Get, Query, Param } from '@nestjs/common'; import { CatalogService } from './catalog.service'; import { CatalogResponseDto } from './dto/catalog-response.dto'; +import { CatalogFiltersDto } from './dto/catalog-filters.dto'; import { CatalogItemShortDto, CatalogItemLongDto, } from './dto/catalog-item.dto'; -import { CatalogParamsDto } from './dto/catalog-params.dto'; import { ApiQuery, ApiResponse, @@ -18,44 +18,36 @@ import { MainPageCatalogResponseDto } from './dto/main-page-catalog-response.dto export class CatalogController { constructor(private readonly catalogService: CatalogService) {} - @Get() - @ApiOperation({ summary: 'Get catalog with filters, search and sorting' }) + @Get('filter') + @ApiOperation({ summary: 'Filter catalog items with query parameters' }) @ApiResponse({ status: 200, - description: 'Catalog items with filters applied', + description: 'Filtered catalog items', type: CatalogResponseDto, }) - @ApiQuery({ - name: 'params', - required: false, - description: 'JSON string of filter parameters', - type: String, - }) - async getCatalog( - @Query('params') paramsString?: string, + @ApiQuery({ name: 'search', required: false, type: String }) + @ApiQuery({ name: 'minLength', required: false, type: Number }) + @ApiQuery({ name: 'maxLength', required: false, type: Number }) + @ApiQuery({ name: 'minPrice', required: false, type: Number }) + @ApiQuery({ name: 'maxPrice', required: false, type: Number }) + @ApiQuery({ name: 'minYear', required: false, type: Number }) + @ApiQuery({ name: 'maxYear', required: false, type: Number }) + @ApiQuery({ name: 'guests', required: false, type: Number }) + @ApiQuery({ name: 'sortByPrice', required: false, type: String }) + @ApiQuery({ name: 'paymentType', required: false, type: String }) + @ApiQuery({ name: 'quickBooking', required: false, type: Boolean }) + @ApiQuery({ name: 'hasToilet', required: false, type: Boolean }) + @ApiQuery({ name: 'date', required: false, type: Date }) + @ApiQuery({ name: 'departureTime', required: false, type: String }) + @ApiQuery({ name: 'arrivalTime', required: false, type: String }) + async getFilteredCatalog( + @Query() filters: CatalogFiltersDto, ): Promise { - let params: CatalogParamsDto = {}; - - if (paramsString) { - try { - params = JSON.parse(decodeURIComponent(paramsString)); - } catch (error) { - params = {}; - } - } - - return this.catalogService.getCatalog(params); + return this.catalogService.getCatalog(filters); } - @Get('main-page') - @ApiOperation({ summary: 'Get catalog for main page' }) - @ApiProperty({ type: MainPageCatalogResponseDto }) - async getMainPageCatalog(): Promise { - return this.catalogService.getMainPageCatalog(); - } - - @Get('all') - @ApiOperation({ summary: 'Get all catalog items without filters' }) + @Get() + @ApiOperation({ summary: 'Get all catalog items (deprecated, use /filter)' }) @ApiResponse({ status: 200, description: 'All catalog items', @@ -65,12 +57,11 @@ export class CatalogController { return this.catalogService.getAllCatalogItems(); } - @Get('filtered') - @ApiOperation({ summary: 'Get catalog with explicit query parameters' }) - async getFilteredCatalog( - @Query() params: CatalogParamsDto, - ): Promise { - return this.catalogService.getCatalog(params); + @Get('main-page') + @ApiOperation({ summary: 'Get catalog for main page' }) + @ApiProperty({ type: MainPageCatalogResponseDto }) + async getMainPageCatalog(): Promise { + return this.catalogService.getMainPageCatalog(); } @Get(':id') diff --git a/src/catalog/catalog.service.ts b/src/catalog/catalog.service.ts index b7b4d65..3628b67 100644 --- a/src/catalog/catalog.service.ts +++ b/src/catalog/catalog.service.ts @@ -9,6 +9,7 @@ import { import { CatalogParamsDto } from './dto/catalog-params.dto'; import { CatalogResponseDto } from './dto/catalog-response.dto'; import { MainPageCatalogResponseDto } from './dto/main-page-catalog-response.dto'; +import { CatalogFiltersDto } from './dto/catalog-filters.dto'; @Injectable() export class CatalogService { @@ -493,46 +494,116 @@ export class CatalogService { return null; } - async getCatalog(params?: CatalogParamsDto): Promise { + async getCatalog(filters?: CatalogFiltersDto): Promise { + // Start with all items converted to short DTO let filteredItems = this.catalogItems.map((item) => this.toShortDto(item)); - if (params?.search) { - const searchTerm = params.search.toLowerCase(); - filteredItems = filteredItems.filter((item) => - item.name.toLowerCase().includes(searchTerm), + // Get the full items for filtering by properties not in short DTO + const fullItems = this.catalogItems; + + // Search filter + if (filters?.search) { + const searchTerm = filters.search.toLowerCase(); + filteredItems = filteredItems.filter((item) => { + const fullItem = fullItems.find((fi) => fi.id === item.id); + return ( + item.name.toLowerCase().includes(searchTerm) || + (fullItem && fullItem.description.toLowerCase().includes(searchTerm)) + ); + }); + } + + // Length filter + if (filters?.minLength !== undefined) { + filteredItems = filteredItems.filter( + (item) => item.length >= filters.minLength!, + ); + } + if (filters?.maxLength !== undefined) { + filteredItems = filteredItems.filter( + (item) => item.length <= filters.maxLength!, ); } - if (params?.filter?.price) { - const { minvalue, maxvalue } = params.filter.price; + // Price filter + if (filters?.minPrice !== undefined) { + filteredItems = filteredItems.filter( + (item) => item.minCost >= filters.minPrice!, + ); + } + if (filters?.maxPrice !== undefined) { + filteredItems = filteredItems.filter( + (item) => item.minCost <= filters.maxPrice!, + ); + } + + // Year filter + if (filters?.minYear !== undefined || filters?.maxYear !== undefined) { filteredItems = filteredItems.filter((item) => { - if (minvalue !== undefined && item.minCost < minvalue) return false; - if (maxvalue !== undefined && item.minCost > maxvalue) return false; + const fullItem = fullItems.find((fi) => fi.id === item.id); + if (!fullItem) return false; + + if (filters.minYear !== undefined && fullItem.year < filters.minYear) + return false; + if (filters.maxYear !== undefined && fullItem.year > filters.maxYear) + return false; return true; }); } - if (params?.sort?.field) { - const { field, direction = 'asc' } = params.sort; - filteredItems.sort((a, b) => { - let valueA = a[field]; - let valueB = b[field]; - - if (typeof valueA === 'string') valueA = valueA.toLowerCase(); - if (typeof valueB === 'string') valueB = valueB.toLowerCase(); - - if (valueA < valueB) return direction === 'asc' ? -1 : 1; - if (valueA > valueB) return direction === 'asc' ? 1 : -1; - return 0; + if (filters?.guests !== undefined) { + const totalPeople = filters?.guests!; + filteredItems = filteredItems.filter((item) => { + const fullItem = fullItems.find((fi) => fi.id === item.id); + return fullItem && fullItem.maxCapacity >= totalPeople; }); } + // Quick booking filter + if (filters?.quickBooking !== undefined) { + filteredItems = filteredItems.filter( + (item) => item.hasQuickRent === filters.quickBooking, + ); + } + + // Has toilet filter (check if cabinsCount > 0) + if (filters?.hasToilet !== undefined) { + filteredItems = filteredItems.filter((item) => { + const fullItem = fullItems.find((fi) => fi.id === item.id); + if (!fullItem) return false; + return filters.hasToilet + ? fullItem.cabinsCount > 0 + : fullItem.cabinsCount === 0; + }); + } + + // Payment type filter (assuming all accept card payments) + if (filters?.paymentType) { + // For now, all yachts accept card payments + // You could add a paymentTypes array to CatalogItemLongDto if needed + console.log('Payment type filtering:', filters.paymentType); + } + + // Date/time filter (check reservations) + if (filters?.date || filters?.departureTime || filters?.arrivalTime) { + // This is more complex - would need to check reservations + // For now, we'll return all items + console.log('Date/time filtering available for future implementation'); + } + + if (filters?.sortByPrice === 'asc') { + filteredItems.sort((a, b) => a.minCost - b.minCost); + } else if (filters?.sortByPrice === 'desc') { + filteredItems.sort((a, b) => b.minCost - a.minCost); + } else { + filteredItems.sort((a, b) => a.name.localeCompare(b.name)); + } + return { items: filteredItems, total: filteredItems.length, - filters: params?.filter, - sort: params?.sort, - search: params?.search, + filters, + search: filters?.search, }; } diff --git a/src/catalog/dto/catalog-filters.dto.ts b/src/catalog/dto/catalog-filters.dto.ts new file mode 100644 index 0000000..5c613dd --- /dev/null +++ b/src/catalog/dto/catalog-filters.dto.ts @@ -0,0 +1,146 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; +import { + IsOptional, + IsString, + IsNumber, + IsBoolean, + IsDate, + Min, +} from 'class-validator'; + +export class CatalogFiltersDto { + @ApiProperty({ required: false, description: 'Search by yacht name' }) + @IsOptional() + @IsString() + search?: string; + + @ApiProperty({ + required: false, + description: 'Minimum length in meters', + example: 10, + }) + @IsOptional() + @IsNumber() + @Min(0) + @Type(() => Number) + minLength?: number; + + @ApiProperty({ + required: false, + description: 'Maximum length in meters', + example: 20, + }) + @IsOptional() + @IsNumber() + @Min(0) + @Type(() => Number) + maxLength?: number; + + @ApiProperty({ + required: false, + description: 'Minimum price', + example: 30000, + }) + @IsOptional() + @IsNumber() + @Min(0) + @Type(() => Number) + minPrice?: number; + + @ApiProperty({ + required: false, + description: 'Maximum price', + example: 100000, + }) + @IsOptional() + @IsNumber() + @Min(0) + @Type(() => Number) + maxPrice?: number; + + @ApiProperty({ + required: false, + description: 'Sort by price', + example: 100000, + }) + @IsOptional() + @IsNumber() + @Min(0) + @Type(() => Number) + sortByPrice?: string; + + @ApiProperty({ required: false, description: 'Minimum year', example: 2019 }) + @IsOptional() + @IsNumber() + @Min(1900) + @Type(() => Number) + minYear?: number; + + @ApiProperty({ required: false, description: 'Maximum year', example: 2023 }) + @IsOptional() + @IsNumber() + @Min(1900) + @Type(() => Number) + maxYear?: number; + + @ApiProperty({ required: false, description: 'Number of guests', example: 4 }) + @IsOptional() + @IsNumber() + @Min(1) + @Type(() => Number) + guests?: number; + + @ApiProperty({ + required: false, + description: 'Payment type', + example: 'card', + }) + @IsOptional() + @IsString() + paymentType?: string; + + @ApiProperty({ + required: false, + description: 'Quick booking available', + example: true, + }) + @IsOptional() + @IsBoolean() + @Type(() => Boolean) + quickBooking?: boolean; + + @ApiProperty({ required: false, description: 'Has toilet', example: true }) + @IsOptional() + @IsBoolean() + @Type(() => Boolean) + hasToilet?: boolean; + + @ApiProperty({ + required: false, + description: 'Departure date', + example: '2025-12-20', + }) + @IsOptional() + @IsDate() + @Type(() => Date) + date?: Date; + + @ApiProperty({ + required: false, + description: 'Departure time', + example: '08:00', + }) + @IsOptional() + @IsString() + departureTime?: string; + + @ApiProperty({ + required: false, + description: 'Arrival time', + example: '20:00', + }) + @IsOptional() + @IsString() + arrivalTime?: string; +} diff --git a/src/catalog/dto/catalog-item.dto.ts b/src/catalog/dto/catalog-item.dto.ts index 05a7cd1..27a2641 100644 --- a/src/catalog/dto/catalog-item.dto.ts +++ b/src/catalog/dto/catalog-item.dto.ts @@ -1,31 +1,133 @@ -import { type Reservation } from 'src/reservations/reservations.service'; -import { type Review } from 'src/reviews/reviews.service'; -import { User } from 'src/users/user.entity'; +import { ApiProperty } from '@nestjs/swagger'; +export class UserDto { + @ApiProperty({ example: 1 }) + userId: number; + + @ApiProperty({ example: 'Иван' }) + firstName: string; + + @ApiProperty({ example: 'Андреев' }) + lastName: string; + + @ApiProperty({ example: '+79009009090' }) + phone: string; + + @ApiProperty({ example: 'ivan@yachting.ru' }) + email: string; + + @ApiProperty({ example: 'Северный Флот', required: false }) + companyName?: string; + + @ApiProperty({ example: 1234567890, required: false }) + inn?: number; + + @ApiProperty({ example: 1122334455667, required: false }) + ogrn?: number; +} + +export class ReviewDto { + @ApiProperty({ example: 1 }) + id: number; + + @ApiProperty({ example: 1 }) + reviewerId: number; + + @ApiProperty({ example: 1 }) + yachtId: number; + + @ApiProperty({ example: 5 }) + starsCount: number; + + @ApiProperty({ example: 'Excellent yacht!' }) + description: string; +} + +export class ReservationDto { + @ApiProperty({ example: 1 }) + id: number; + + @ApiProperty({ example: 1 }) + yachtId: number; + + @ApiProperty({ example: 1 }) + reservatorId: number; + + @ApiProperty({ example: 1733097600 }) + startUtc: number; + + @ApiProperty({ example: 1733133600 }) + endUtc: number; +} export class CatalogItemShortDto { + @ApiProperty({ example: 1 }) id?: number; + + @ApiProperty({ example: 'Азимут 55' }) name: string; + + @ApiProperty({ example: 16.7 }) length: number; + + @ApiProperty({ example: 32 }) speed: number; + + @ApiProperty({ example: 85000 }) minCost: number; + + @ApiProperty({ example: 'api/uploads/1765727362318-238005198.jpg' }) mainImageUrl: string; + + @ApiProperty({ + type: [String], + example: ['api/uploads/1765727362318-238005198.jpg'], + }) galleryUrls: string[]; + + @ApiProperty({ example: true }) hasQuickRent: boolean; + + @ApiProperty({ example: true }) isFeatured: boolean; + + @ApiProperty({ required: false, example: '🔥 Лучшее предложение' }) topText?: string; + + @ApiProperty({ required: false, example: true }) isBestOffer?: boolean; } export class CatalogItemLongDto extends CatalogItemShortDto { + @ApiProperty({ example: 2022 }) year: number; + + @ApiProperty({ example: 8 }) comfortCapacity: number; + + @ApiProperty({ example: 12 }) maxCapacity: number; + + @ApiProperty({ example: 4.8 }) width: number; + + @ApiProperty({ example: 3 }) cabinsCount: number; + + @ApiProperty({ example: 'Стеклопластик' }) matherial: string; + + @ApiProperty({ example: 1200 }) power: number; + + @ApiProperty({ example: 'Роскошная моторная яхта...' }) description: string; - owner: User; - reviews: Review[]; - reservations: Reservation[]; + + @ApiProperty({ type: UserDto }) + owner: UserDto; + + @ApiProperty({ type: [ReviewDto] }) + reviews: ReviewDto[]; + + @ApiProperty({ type: [ReservationDto] }) + reservations: ReservationDto[]; } diff --git a/src/catalog/dto/catalog-response.dto.ts b/src/catalog/dto/catalog-response.dto.ts index f0da60b..3ec7cf9 100644 --- a/src/catalog/dto/catalog-response.dto.ts +++ b/src/catalog/dto/catalog-response.dto.ts @@ -1,11 +1,29 @@ +import { ApiProperty } from '@nestjs/swagger'; import { CatalogItemShortDto } from './catalog-item.dto'; +import { CatalogFiltersDto } from './catalog-filters.dto'; export class CatalogResponseDto { + @ApiProperty({ type: [CatalogItemShortDto] }) items: CatalogItemShortDto[]; + + @ApiProperty({ example: 42 }) total: number; + + @ApiProperty({ required: false, example: 1 }) page?: number; + + @ApiProperty({ required: false, example: 10 }) limit?: number; - filters?: any; + + @ApiProperty({ type: CatalogFiltersDto, required: false }) + filters?: CatalogFiltersDto; + + @ApiProperty({ + required: false, + example: { field: 'name', direction: 'asc' }, + }) sort?: any; + + @ApiProperty({ required: false, example: 'yacht' }) search?: string; }