Add long yacht + reservations + reviews

This commit is contained in:
Иван 2025-12-14 21:58:27 +03:00
parent 4b7c100b3d
commit 11d59be696
22 changed files with 561 additions and 152 deletions

View File

@ -6,9 +6,11 @@ import { CatalogModule } from './catalog/catalog.module';
import { AuthModule } from './auth/auth.module'; import { AuthModule } from './auth/auth.module';
import { UsersModule } from './users/users.module'; import { UsersModule } from './users/users.module';
import { FilesModule } from './files/files.module'; import { FilesModule } from './files/files.module';
import { ReviewsModule } from './reviews/reviews.module';
import { ReservationsModule } from './reservations/reservations.module';
@Module({ @Module({
imports: [YachtsModule, CatalogModule, AuthModule, UsersModule, FilesModule], imports: [YachtsModule, CatalogModule, AuthModule, UsersModule, FilesModule, ReviewsModule, ReservationsModule],
controllers: [AppController], controllers: [AppController],
providers: [AppService], providers: [AppService],
}) })

View File

@ -1,18 +0,0 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AuthService } from './auth.service';
describe('AuthService', () => {
let service: AuthService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [AuthService],
}).compile();
service = module.get<AuthService>(AuthService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

View File

@ -1,18 +0,0 @@
import { Test, TestingModule } from '@nestjs/testing';
import { CatalogController } from './catalog.controller';
describe('CatalogController', () => {
let controller: CatalogController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [CatalogController],
}).compile();
controller = module.get<CatalogController>(CatalogController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});

View File

@ -1,7 +1,10 @@
import { Controller, Get, Query } from '@nestjs/common'; import { Controller, Get, Query, Param } from '@nestjs/common';
import { CatalogService } from './catalog.service'; import { CatalogService } from './catalog.service';
import { CatalogResponseDto } from './dto/catalog-response.dto'; import { CatalogResponseDto } from './dto/catalog-response.dto';
import { CatalogItemDto } from './dto/catalog-item.dto'; import {
CatalogItemShortDto,
CatalogItemLongDto,
} from './dto/catalog-item.dto';
import { CatalogParamsDto } from './dto/catalog-params.dto'; import { CatalogParamsDto } from './dto/catalog-params.dto';
import { import {
ApiQuery, ApiQuery,
@ -56,9 +59,9 @@ export class CatalogController {
@ApiResponse({ @ApiResponse({
status: 200, status: 200,
description: 'All catalog items', description: 'All catalog items',
type: [CatalogItemDto], type: [CatalogItemShortDto],
}) })
async getAllCatalogItems(): Promise<CatalogItemDto[]> { async getAllCatalogItems(): Promise<CatalogItemShortDto[]> {
return this.catalogService.getAllCatalogItems(); return this.catalogService.getAllCatalogItems();
} }
@ -69,4 +72,17 @@ export class CatalogController {
): Promise<CatalogResponseDto> { ): Promise<CatalogResponseDto> {
return this.catalogService.getCatalog(params); return this.catalogService.getCatalog(params);
} }
@Get(':id')
@ApiOperation({ summary: 'Get catalog item by ID with full details' })
@ApiResponse({
status: 200,
description: 'Catalog item with reviews, reservations and owner',
type: CatalogItemLongDto,
})
async getCatalogItemById(
@Param('id') id: string,
): Promise<CatalogItemLongDto | null> {
return this.catalogService.getCatalogItemById(Number(id));
}
} }

View File

@ -1,18 +0,0 @@
import { Test, TestingModule } from '@nestjs/testing';
import { CatalogService } from './catalog.service';
describe('CatalogService', () => {
let service: CatalogService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [CatalogService],
}).compile();
service = module.get<CatalogService>(CatalogService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

View File

@ -1,15 +1,27 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { CatalogItemDto } from './dto/catalog-item.dto'; import { UsersService } from '../users/users.service';
import { ReservationsService } from '../reservations/reservations.service';
import { ReviewsService } from '../reviews/reviews.service';
import {
CatalogItemShortDto,
CatalogItemLongDto,
} from './dto/catalog-item.dto';
import { CatalogParamsDto } from './dto/catalog-params.dto'; import { CatalogParamsDto } from './dto/catalog-params.dto';
import { CatalogResponseDto } from './dto/catalog-response.dto'; import { CatalogResponseDto } from './dto/catalog-response.dto';
import { MainPageCatalogResponseDto } from './dto/main-page-catalog-response.dto'; import { MainPageCatalogResponseDto } from './dto/main-page-catalog-response.dto';
@Injectable() @Injectable()
export class CatalogService { export class CatalogService {
private catalogItems: CatalogItemDto[] = [ constructor(
private readonly usersService: UsersService,
private readonly reservationsService: ReservationsService,
private readonly reviewsService: ReviewsService,
) {}
private catalogItems: CatalogItemLongDto[] = [
{ {
id: 1, id: 1,
name: 'Azimut 55', name: 'Азимут 55',
length: 16.7, length: 16.7,
speed: 32, speed: 32,
minCost: 85000, minCost: 85000,
@ -27,10 +39,22 @@ export class CatalogService {
], ],
hasQuickRent: true, hasQuickRent: true,
isFeatured: true, isFeatured: true,
year: 2022,
comfortCapacity: 8,
maxCapacity: 12,
width: 4.8,
cabinsCount: 3,
matherial: 'Стеклопластик',
power: 1200,
description:
'Роскошная моторная яхта Азимут 55 - это воплощение итальянского стиля и российского качества. Идеально подходит для прогулок по Финскому заливу, корпоративных мероприятий и романтических свиданий. На борту: три комфортабельные каюты, просторный салон с панорамным остеклением, полностью оборудованная кухня и две ванные комнаты. Максимальная скорость 32 узла позволяет быстро добраться до самых живописных мест Карельского перешейка.',
owner: null as any,
reviews: [],
reservations: [],
}, },
{ {
id: 2, id: 2,
name: 'Sunseeker Manhattan 52', name: 'Сансикер Манхэттен 52',
length: 15.8, length: 15.8,
speed: 34, speed: 34,
minCost: 92000, minCost: 92000,
@ -49,10 +73,22 @@ export class CatalogService {
hasQuickRent: false, hasQuickRent: false,
isFeatured: false, isFeatured: false,
topText: '🔥 Лучшее предложение', topText: '🔥 Лучшее предложение',
year: 2023,
comfortCapacity: 6,
maxCapacity: 10,
width: 4.5,
cabinsCount: 3,
matherial: 'Стеклопластик',
power: 1400,
description:
'Британский шик и русская душа в одной яхте! Сансикер Манхэттен 52 - выбор настоящих ценителей морских путешествий. Просторный кокпит с мягкими диванами, бар на 8 персон, система мультимедиа премиум-класса. Идеальна для празднования дня рождения на воде или деловой встречи с партнерами. Отличная маневренность позволяет заходить в марины Санкт-Петербурга и Кронштадта.',
owner: null as any,
reviews: [],
reservations: [],
}, },
{ {
id: 3, id: 3,
name: 'Princess V55', name: 'Принцесс V55',
length: 16.7, length: 16.7,
speed: 33, speed: 33,
minCost: 78000, minCost: 78000,
@ -71,10 +107,22 @@ export class CatalogService {
hasQuickRent: true, hasQuickRent: true,
isFeatured: false, isFeatured: false,
topText: '🍷 Идеальна для заката с бокалом вина', topText: '🍷 Идеальна для заката с бокалом вина',
year: 2021,
comfortCapacity: 8,
maxCapacity: 12,
width: 4.7,
cabinsCount: 4,
matherial: 'Стеклопластик',
power: 1100,
description:
'Принцесс V55 - королева российских вод! Эта яхта создана для тех, кто ценит комфорт и элегантность. Четыре уютные каюты с кондиционером, гальюн с душем, полностью оборудованная камбузная зона. Особенность - огромный платц с гидравлическим трапом для купания в Ладожском озере. Отличный выбор для семейного отдыха или рыбалки с друзьями.',
owner: null as any,
reviews: [],
reservations: [],
}, },
{ {
id: 4, id: 4,
name: 'Ferretti 500', name: 'Ферретти 500',
length: 15.2, length: 15.2,
speed: 31, speed: 31,
minCost: 68000, minCost: 68000,
@ -93,10 +141,22 @@ export class CatalogService {
hasQuickRent: true, hasQuickRent: true,
isFeatured: false, isFeatured: false,
topText: '⏳ Часто бронируется - успей', topText: '⏳ Часто бронируется - успей',
year: 2020,
comfortCapacity: 6,
maxCapacity: 8,
width: 4.3,
cabinsCount: 3,
matherial: 'Стеклопластик',
power: 900,
description:
'Итальянская страсть в русской стихии! Ферретти 500 сочетает в себе средиземноморский шарм и надежность для суровых условий Балтики. Просторный салон с панорамными окнами, обеденная зона на 6 человек, современная навигационная система. Идеально подходит для фотосессий на фоне разводных мостов Петербурга или романтического ужина под звуки волн.',
owner: null as any,
reviews: [],
reservations: [],
}, },
{ {
id: 5, id: 5,
name: 'Sea Ray 510 Sundancer', name: 'Си Рей 510 Сандансер',
length: 15.5, length: 15.5,
speed: 35, speed: 35,
minCost: 72000, minCost: 72000,
@ -114,10 +174,22 @@ export class CatalogService {
], ],
hasQuickRent: false, hasQuickRent: false,
isFeatured: false, isFeatured: false,
year: 2023,
comfortCapacity: 8,
maxCapacity: 10,
width: 4.6,
cabinsCount: 3,
matherial: 'Стеклопластик',
power: 1300,
description:
'Американская мощь для русского моря! Си Рей 510 Сандансер - самая быстрая яхта в нашем флоте. Развивает скорость до 35 узлов, что позволяет за день обогнуть весь Финский залив. Три комфортабельные каюты, система стабилизации на стоянке, мощная аудиосистема с сабвуфером. Отличный выбор для любителей острых ощущений и скоростных прогулок.',
owner: null as any,
reviews: [],
reservations: [],
}, },
{ {
id: 6, id: 6,
name: 'Bavaria SR41', name: 'Бавария SR41',
length: 12.5, length: 12.5,
speed: 28, speed: 28,
minCost: 45000, minCost: 45000,
@ -135,10 +207,22 @@ export class CatalogService {
], ],
hasQuickRent: true, hasQuickRent: true,
isFeatured: false, isFeatured: false,
year: 2019,
comfortCapacity: 6,
maxCapacity: 8,
width: 3.9,
cabinsCount: 2,
matherial: 'Стеклопластик',
power: 320,
description:
'Немецкое качество для русского характера! Бавария SR41 - надежная и экономичная яхта для спокойных прогулок по Ладоге. Две уютные каюты, просторный кокпит с тентом от дождя, лебедка для подъема парусов. Идеальный выбор для начинающих яхтсменов или семейного отдыха с детьми. Расход топлива всего 15 литров в час!',
owner: null as any,
reviews: [],
reservations: [],
}, },
{ {
id: 7, id: 7,
name: 'Jeanneau Merry Fisher 895', name: 'Жанно Мери Фишер 895',
length: 8.9, length: 8.9,
speed: 25, speed: 25,
minCost: 32000, minCost: 32000,
@ -156,10 +240,22 @@ export class CatalogService {
], ],
hasQuickRent: true, hasQuickRent: true,
isFeatured: false, isFeatured: false,
year: 2022,
comfortCapacity: 4,
maxCapacity: 6,
width: 3.0,
cabinsCount: 1,
matherial: 'Стеклопластик',
power: 250,
description:
'Французская элегантность для русского простора! Жанно Мери Фишер 895 - компактная, но вместительная яхта для рыбалки и пикников. Одна просторная каюта, открытый кокпит, столик для барбекю. Отлично подходит для выездов на природу, ночевки в бухтах или обучения детей управлению яхтой. Самый экономичный вариант в нашем флоте!',
owner: null as any,
reviews: [],
reservations: [],
}, },
{ {
id: 8, id: 8,
name: 'Beneteau Swift Trawler 41', name: 'Бенето Свифт Троулер 41',
length: 12.5, length: 12.5,
speed: 22, speed: 22,
minCost: 55000, minCost: 55000,
@ -177,10 +273,22 @@ export class CatalogService {
], ],
hasQuickRent: false, hasQuickRent: false,
isFeatured: false, isFeatured: false,
year: 2021,
comfortCapacity: 6,
maxCapacity: 8,
width: 4.2,
cabinsCount: 2,
matherial: 'Стеклопластик',
power: 425,
description:
'Французский траулер для русского севера! Бенето Свифт Троулер 41 создан для длительных путешествий по Белому морю. Экономичный дизельный двигатель, большой запас топлива, система опреснения воды. Две комфортабельные каюты с подогревом пола. Идеальный выбор для экспедиций или многодневных круизов по северным островам.',
owner: null as any,
reviews: [],
reservations: [],
}, },
{ {
id: 9, id: 9,
name: 'Lagoon 450', name: 'Лагун 450',
length: 13.5, length: 13.5,
speed: 20, speed: 20,
minCost: 65000, minCost: 65000,
@ -198,10 +306,22 @@ export class CatalogService {
], ],
hasQuickRent: true, hasQuickRent: true,
isFeatured: false, isFeatured: false,
year: 2020,
comfortCapacity: 8,
maxCapacity: 10,
width: 7.8,
cabinsCount: 4,
matherial: 'Стеклопластик',
power: 90,
description:
'Французский катамаран для русского размаха! Лагун 450 - невероятно устойчивая и просторная яхта. Четыре отдельные каюты с санузлами, огромный салон-трансформер, две кухни. Идеально подходит для больших компаний, свадебных церемоний на воде или длительных круизов всей семьей. Не кренится даже в шторм!',
owner: null as any,
reviews: [],
reservations: [],
}, },
{ {
id: 10, id: 10,
name: 'Fountaine Pajot Lucia 40', name: 'Фонтен Пажо Люсия 40',
length: 11.7, length: 11.7,
speed: 18, speed: 18,
minCost: 58000, minCost: 58000,
@ -219,10 +339,22 @@ export class CatalogService {
], ],
hasQuickRent: true, hasQuickRent: true,
isFeatured: false, isFeatured: false,
year: 2023,
comfortCapacity: 8,
maxCapacity: 10,
width: 7.1,
cabinsCount: 4,
matherial: 'Стеклопластик',
power: 80,
description:
'Французский катамаран класса люкс! Фонтен Пажо Люсия 40 - это плавающий пятизвездочный отель. Четыре каюты-люкс с джакузи, салон с камином, профессиональная кухня с шеф-поваром. Система стабилизации на якоре, гидромассажный бассейн на палубе. Выбор настоящих ценителей роскоши и комфорта на воде.',
owner: null as any,
reviews: [],
reservations: [],
}, },
{ {
id: 11, id: 11,
name: 'Dufour 460', name: 'Дюфур 460',
length: 14.1, length: 14.1,
speed: 26, speed: 26,
minCost: 62000, minCost: 62000,
@ -240,10 +372,22 @@ export class CatalogService {
], ],
hasQuickRent: false, hasQuickRent: false,
isFeatured: false, isFeatured: false,
year: 2022,
comfortCapacity: 8,
maxCapacity: 10,
width: 4.5,
cabinsCount: 3,
matherial: 'Стеклопластик',
power: 380,
description:
'Французская парусная яхта для русского ветра! Дюфур 460 - мечта любого яхтсмена. Три просторные каюты, кокпит с мягкими сиденьями, современное парусное вооружение. Идеально сбалансированная, легко управляется даже новичками. Отличный выбор для регат, обучения парусному спорту или романтических круизов под парусами.',
owner: null as any,
reviews: [],
reservations: [],
}, },
{ {
id: 12, id: 12,
name: 'Grand Banks 60', name: 'Гранд Бэнкс 60',
length: 18.3, length: 18.3,
speed: 24, speed: 24,
minCost: 125000, minCost: 125000,
@ -261,9 +405,65 @@ export class CatalogService {
], ],
hasQuickRent: true, hasQuickRent: true,
isFeatured: false, isFeatured: false,
year: 2023,
comfortCapacity: 6,
maxCapacity: 8,
width: 5.2,
cabinsCount: 3,
matherial: 'Стеклопластик',
power: 1600,
description:
'Американская легенда для русского океана! Гранд Бэнкс 60 - экспедиционная яхта для самых смелых путешествий. Три каюты-люкс, салон с библиотекой, зимний сад, сауна. Автономность плавания - 30 дней! Способна пересечь Баренцево море и дойти до Шпицбергена. Выбор для настоящих морских волков и исследователей Арктики.',
owner: null as any,
reviews: [],
reservations: [],
}, },
]; ];
private toShortDto(item: CatalogItemLongDto): CatalogItemShortDto {
const {
id,
name,
length,
speed,
minCost,
mainImageUrl,
galleryUrls,
hasQuickRent,
isFeatured,
topText,
} = item;
return {
id,
name,
length,
speed,
minCost,
mainImageUrl,
galleryUrls,
hasQuickRent,
isFeatured,
topText,
};
}
async getCatalogItemById(id: number): Promise<CatalogItemLongDto | null> {
const item = this.catalogItems.find((item) => item.id === id);
if (!item) return null;
const ownerId = [1, 2][(id - 1) % 2];
const owner = await this.usersService.findById(ownerId);
if (owner) {
item.owner = owner;
}
item.reviews = this.reviewsService.getReviewsByYachtId(id);
item.reservations = this.reservationsService.getReservationsByYachtId(id);
return item;
}
async getMainPageCatalog(): Promise<MainPageCatalogResponseDto | null> { async getMainPageCatalog(): Promise<MainPageCatalogResponseDto | null> {
const clonedCatalog = [...this.catalogItems]; const clonedCatalog = [...this.catalogItems];
const filteredCatalog = clonedCatalog.filter( const filteredCatalog = clonedCatalog.filter(
@ -280,12 +480,12 @@ export class CatalogService {
); );
const mappedRestYachts = filteredCatalog.slice(0, 6).map((item) => ({ const mappedRestYachts = filteredCatalog.slice(0, 6).map((item) => ({
...item, ...this.toShortDto(item),
isBestOffer: item.minCost === minCost, isBestOffer: item.minCost === minCost,
})); }));
return { return {
featuredYacht: clonedCatalog[featuredYachtIndex], featuredYacht: this.toShortDto(clonedCatalog[featuredYachtIndex]),
restYachts: mappedRestYachts, restYachts: mappedRestYachts,
}; };
} }
@ -294,11 +494,10 @@ export class CatalogService {
} }
async getCatalog(params?: CatalogParamsDto): Promise<CatalogResponseDto> { async getCatalog(params?: CatalogParamsDto): Promise<CatalogResponseDto> {
let filteredItems = [...this.catalogItems]; let filteredItems = this.catalogItems.map((item) => this.toShortDto(item));
if (params?.search) { if (params?.search) {
const searchTerm = params.search.toLowerCase(); const searchTerm = params.search.toLowerCase();
filteredItems = filteredItems.filter((item) => filteredItems = filteredItems.filter((item) =>
item.name.toLowerCase().includes(searchTerm), item.name.toLowerCase().includes(searchTerm),
); );
@ -306,7 +505,6 @@ export class CatalogService {
if (params?.filter?.price) { if (params?.filter?.price) {
const { minvalue, maxvalue } = params.filter.price; const { minvalue, maxvalue } = params.filter.price;
filteredItems = filteredItems.filter((item) => { filteredItems = filteredItems.filter((item) => {
if (minvalue !== undefined && item.minCost < minvalue) return false; if (minvalue !== undefined && item.minCost < minvalue) return false;
if (maxvalue !== undefined && item.minCost > maxvalue) return false; if (maxvalue !== undefined && item.minCost > maxvalue) return false;
@ -316,7 +514,6 @@ export class CatalogService {
if (params?.sort?.field) { if (params?.sort?.field) {
const { field, direction = 'asc' } = params.sort; const { field, direction = 'asc' } = params.sort;
filteredItems.sort((a, b) => { filteredItems.sort((a, b) => {
let valueA = a[field]; let valueA = a[field];
let valueB = b[field]; let valueB = b[field];
@ -326,7 +523,6 @@ export class CatalogService {
if (valueA < valueB) return direction === 'asc' ? -1 : 1; if (valueA < valueB) return direction === 'asc' ? -1 : 1;
if (valueA > valueB) return direction === 'asc' ? 1 : -1; if (valueA > valueB) return direction === 'asc' ? 1 : -1;
return 0; return 0;
}); });
} }
@ -340,7 +536,7 @@ export class CatalogService {
}; };
} }
async getAllCatalogItems(): Promise<CatalogItemDto[]> { async getAllCatalogItems(): Promise<CatalogItemShortDto[]> {
return this.catalogItems; return this.catalogItems.map((item) => this.toShortDto(item));
} }
} }

View File

@ -1,4 +1,8 @@
export class CatalogItemDto { import { type Reservation } from 'src/reservations/reservations.service';
import { type Review } from 'src/reviews/reviews.service';
import { User } from 'src/users/user.entity';
export class CatalogItemShortDto {
id?: number; id?: number;
name: string; name: string;
length: number; length: number;
@ -11,3 +15,17 @@ export class CatalogItemDto {
topText?: string; topText?: string;
isBestOffer?: boolean; isBestOffer?: boolean;
} }
export class CatalogItemLongDto extends CatalogItemShortDto {
year: number;
comfortCapacity: number;
maxCapacity: number;
width: number;
cabinsCount: number;
matherial: string;
power: number;
description: string;
owner: User;
reviews: Review[];
reservations: Reservation[];
}

View File

@ -1,7 +1,7 @@
import { CatalogItemDto } from './catalog-item.dto'; import { CatalogItemShortDto } from './catalog-item.dto';
export class CatalogResponseDto { export class CatalogResponseDto {
items: CatalogItemDto[]; items: CatalogItemShortDto[];
total: number; total: number;
page?: number; page?: number;
limit?: number; limit?: number;

View File

@ -1,6 +1,6 @@
import { CatalogItemDto } from './catalog-item.dto'; import { CatalogItemShortDto } from './catalog-item.dto';
export class MainPageCatalogResponseDto { export class MainPageCatalogResponseDto {
featuredYacht: CatalogItemDto; featuredYacht: CatalogItemShortDto;
restYachts: CatalogItemDto[]; restYachts: CatalogItemShortDto[];
} }

View File

@ -0,0 +1,6 @@
export class ReservationItemDto {
yachtId: number;
reservatorId: number;
startUtc: number;
endUtc: number;
}

View File

@ -0,0 +1,28 @@
import { Controller, Get, Post, Body, Param } from '@nestjs/common';
import { ReservationsService } from './reservations.service';
import { ReservationItemDto } from './reservation-item.dto';
@Controller('reservations')
export class ReservationsController {
constructor(private readonly reservationsService: ReservationsService) {}
@Post()
create(@Body() dto: ReservationItemDto) {
return this.reservationsService.createReservation(dto);
}
@Get('user/:userId')
findByUserId(@Param('userId') userId: string) {
return this.reservationsService.getReservationsByUserId(Number(userId));
}
@Get('yacht/:yachtId')
findByYachtId(@Param('yachtId') yachtId: string) {
return this.reservationsService.getReservationsByYachtId(Number(yachtId));
}
@Get()
findAll() {
return this.reservationsService.getAllReservations();
}
}

View File

@ -0,0 +1,9 @@
import { Module } from '@nestjs/common';
import { ReservationsService } from './reservations.service';
import { ReservationsController } from './reservations.controller';
@Module({
controllers: [ReservationsController],
providers: [ReservationsService],
})
export class ReservationsModule {}

View File

@ -0,0 +1,78 @@
import { Injectable } from '@nestjs/common';
import { ReservationItemDto } from './reservation-item.dto';
export interface Reservation extends ReservationItemDto {
id: number;
}
@Injectable()
export class ReservationsService {
private reservations: Reservation[] = [
{
id: 1,
yachtId: 1,
reservatorId: 1,
startUtc: 1733097600,
endUtc: 1733133600,
},
{
id: 2,
yachtId: 3,
reservatorId: 2,
startUtc: 1733212800,
endUtc: 1733436000,
},
{
id: 3,
yachtId: 5,
reservatorId: 1,
startUtc: 1733860800,
endUtc: 1734048000,
},
{
id: 4,
yachtId: 7,
reservatorId: 2,
startUtc: 1734216000,
endUtc: 1734472800,
},
{
id: 5,
yachtId: 9,
reservatorId: 1,
startUtc: 1734705600,
endUtc: 1734883200,
},
{
id: 6,
yachtId: 11,
reservatorId: 2,
startUtc: 1735113600,
endUtc: 1735336800,
},
];
private idCounter = 7;
createReservation(dto: ReservationItemDto): Reservation {
const reservation = {
id: this.idCounter++,
...dto,
};
this.reservations.push(reservation);
return reservation;
}
getReservationsByUserId(userId: number): Reservation[] {
return this.reservations.filter((r) => r.reservatorId === userId);
}
getReservationsByYachtId(yachtId: number): Reservation[] {
return this.reservations.filter((r) => r.yachtId === yachtId);
}
getAllReservations(): Reservation[] {
return this.reservations;
}
}

View File

@ -0,0 +1,6 @@
export class ReviewItemDto {
reviewerId: number;
yachtId: number;
starsCount: number;
description: string;
}

View File

@ -0,0 +1,28 @@
import { Controller, Get, Post, Body, Param } from '@nestjs/common';
import { ReviewsService } from './reviews.service';
import { ReviewItemDto } from './review-item.dto';
@Controller('reviews')
export class ReviewsController {
constructor(private readonly reviewsService: ReviewsService) {}
@Post()
create(@Body() dto: ReviewItemDto) {
return this.reviewsService.createReview(dto);
}
@Get('user/:userId')
findByUserId(@Param('userId') userId: string) {
return this.reviewsService.getReviewsByUserId(Number(userId));
}
@Get('yacht/:yachtId')
findByYachtId(@Param('yachtId') yachtId: string) {
return this.reviewsService.getReviewsByYachtId(Number(yachtId));
}
@Get()
findAll() {
return this.reviewsService.getAllReviews();
}
}

View File

@ -0,0 +1,9 @@
import { Module } from '@nestjs/common';
import { ReviewsService } from './reviews.service';
import { ReviewsController } from './reviews.controller';
@Module({
controllers: [ReviewsController],
providers: [ReviewsService],
})
export class ReviewsModule {}

View File

@ -0,0 +1,92 @@
import { Injectable } from '@nestjs/common';
import { ReviewItemDto } from './review-item.dto';
export interface Review extends ReviewItemDto {
id: number;
}
@Injectable()
export class ReviewsService {
private reviews: Review[] = [
{
id: 1,
reviewerId: 1,
yachtId: 1,
starsCount: 5,
description: 'Excellent yacht!',
},
{
id: 2,
reviewerId: 2,
yachtId: 1,
starsCount: 4,
description: 'Very good experience',
},
{
id: 3,
reviewerId: 1,
yachtId: 3,
starsCount: 3,
description: 'Average condition',
},
{
id: 4,
reviewerId: 2,
yachtId: 5,
starsCount: 5,
description: 'Perfect for sailing',
},
{
id: 5,
reviewerId: 1,
yachtId: 7,
starsCount: 4,
description: 'Comfortable and fast',
},
{
id: 6,
reviewerId: 2,
yachtId: 9,
starsCount: 2,
description: 'Needs maintenance',
},
{
id: 7,
reviewerId: 1,
yachtId: 11,
starsCount: 5,
description: 'Luxury experience',
},
{
id: 8,
reviewerId: 2,
yachtId: 12,
starsCount: 4,
description: 'Great value for money',
},
];
private idCounter = 9;
createReview(dto: ReviewItemDto): Review {
const review = {
id: this.idCounter++,
...dto,
};
this.reviews.push(review);
return review;
}
getReviewsByUserId(userId: number): Review[] {
return this.reviews.filter((r) => r.reviewerId === userId);
}
getReviewsByYachtId(yachtId: number): Review[] {
return this.reviews.filter((r) => r.yachtId === yachtId);
}
getAllReviews(): Review[] {
return this.reviews;
}
}

View File

@ -8,4 +8,7 @@ export type User = {
email: string; email: string;
password: string; password: string;
yachts?: Yacht[]; yachts?: Yacht[];
companyName?: string;
inn?: number;
ogrn?: number;
}; };

View File

@ -1,18 +0,0 @@
import { Test, TestingModule } from '@nestjs/testing';
import { UsersService } from './users.service';
describe('UsersService', () => {
let service: UsersService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [UsersService],
}).compile();
service = module.get<UsersService>(UsersService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

View File

@ -9,19 +9,47 @@ export class UsersService {
private readonly users: User[] = [ private readonly users: User[] = [
{ {
userId: 1, userId: 1,
firstName: 'Ivan', firstName: 'Иван',
lastName: 'Andreev', lastName: 'Андреев',
phone: '+79009009090', phone: '+79009009090',
email: 'email@email.com', email: 'ivan@yachting.ru',
password: 'admin', password: 'admin',
companyName: 'Северный Флот',
inn: 1234567890,
ogrn: 1122334455667,
}, },
{ {
userId: 2, userId: 2,
firstName: 'Sergey', firstName: 'Сергей',
lastName: 'Bolshakov', lastName: 'Большаков',
phone: '+79009009090', phone: '+79119119191',
email: 'email1@email.com', email: 'sergey@yachting.ru',
password: 'admin', password: 'admin',
companyName: 'Балтийские Просторы',
inn: 9876543210,
ogrn: 9988776655443,
},
{
userId: 3,
firstName: 'Анна',
lastName: 'Петрова',
phone: '+79229229292',
email: 'anna@yachting.ru',
password: 'admin',
companyName: 'Ладожские Ветры',
inn: 5555555555,
ogrn: 3333444455556,
},
{
userId: 4,
firstName: 'Дмитрий',
lastName: 'Соколов',
phone: '+79339339393',
email: 'dmitry@yachting.ru',
password: 'admin',
companyName: 'Финский Залив',
inn: 1111222233,
ogrn: 7777888899990,
}, },
]; ];
@ -32,8 +60,7 @@ export class UsersService {
const user = this.users.find((user) => user.email === email); const user = this.users.find((user) => user.email === email);
if (user && includeYachts) { if (user && includeYachts) {
// Fetch yachts for this user user.yachts = [];
user.yachts = await [];
} }
return user; return user;
@ -57,7 +84,6 @@ export class UsersService {
return this.users; return this.users;
} }
// Fetch all users with their yachts
const usersWithYachts = await Promise.all( const usersWithYachts = await Promise.all(
this.users.map(async (user) => { this.users.map(async (user) => {
const yachts = []; const yachts = [];

View File

@ -1,18 +0,0 @@
import { Test, TestingModule } from '@nestjs/testing';
import { YachtsController } from './yacht.controller';
describe('YachtController', () => {
let controller: YachtsController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [YachtsController],
}).compile();
controller = module.get<YachtsController>(YachtsController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});

View File

@ -1,18 +0,0 @@
import { Test, TestingModule } from '@nestjs/testing';
import { YachtsService } from './yachts.service';
describe('YachtsService', () => {
let service: YachtsService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [YachtsService],
}).compile();
service = module.get<YachtsService>(YachtsService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});