diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index 84f2a6b0ab399a63519caeeeb1c174e93f3faa03..0792d3ccf5447880be019773ee89d472b933aadf 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -30,6 +30,8 @@ import { RouteModule } from './route/route.module'; import { Route } from './route/entities/route.entity'; import { TravelPlaceModule } from './travel-place/travel-place.module'; import { TravelPlace } from './travel-place/entities/travel-place.entity'; +import { VisitedModule } from './visited/visited.module'; +import { Visited } from './visited/entities/visited.entity'; @Module({ imports: [ @@ -54,6 +56,7 @@ import { TravelPlace } from './travel-place/entities/travel-place.entity'; Category, Route, TravelPlace, + Visited, ], synchronize: DbConstants.DB_SYNC, logging: false, @@ -73,6 +76,7 @@ import { TravelPlace } from './travel-place/entities/travel-place.entity'; CategoryModule, RouteModule, TravelPlaceModule, + VisitedModule, ], controllers: [AppController], providers: [AppService, DatabaseSeederModule], diff --git a/backend/src/place/place.service.ts b/backend/src/place/place.service.ts index 904aaa4cfb8a2bead353b45d05e4fdd2016c707b..784232d0da538d9037fa64df21241cf1e95bf8d0 100644 --- a/backend/src/place/place.service.ts +++ b/backend/src/place/place.service.ts @@ -79,7 +79,7 @@ export class PlaceService { } } - async findAllByTown(idTown: number, lang: LANGUAGES) { + async findAllByTown(idTown: number, lang: LANGUAGES): Promise { const res: any[] = await this.dataSource .getRepository(Place) .createQueryBuilder('place') diff --git a/backend/src/route/dto/recommend-route.dto.ts b/backend/src/route/dto/recommend-route.dto.ts index 66e99e4038e4c13248b273103534d304b576cc4a..eddd3c781cf64e2811f9b4d2c912df06c7213eab 100644 --- a/backend/src/route/dto/recommend-route.dto.ts +++ b/backend/src/route/dto/recommend-route.dto.ts @@ -3,5 +3,5 @@ export interface RecommendPlace { openAt: number; closeAt: number; categories: number[]; - grade: number; + rating: number; } diff --git a/backend/src/route/route.controller.ts b/backend/src/route/route.controller.ts index 0d8c512f30627223e3dcb27ba05450f46847eff7..b3162fc3317adff25fd6ae044e4ad6c14e1b6372 100644 --- a/backend/src/route/route.controller.ts +++ b/backend/src/route/route.controller.ts @@ -1,43 +1,46 @@ -import { Controller, Get, Post, Body, Patch, Param, Delete, UseGuards } from '@nestjs/common'; +import { Controller, Get, Post, Body, Patch, Param, Delete, UseGuards, Req, Query } from '@nestjs/common'; import { RouteService } from './route.service'; import { CreateRouteDto } from './dto/create-route.dto'; import { UpdateRouteDto } from './dto/update-route.dto'; -import { ApiConsumes, ApiTags } from '@nestjs/swagger'; +import { ApiBearerAuth, ApiConsumes, ApiParam, ApiQuery, ApiTags } from '@nestjs/swagger'; +import { LANGUAGES } from 'src/shared/enum/languages.enum'; +import { AuthUserGuard } from 'src/auth/user/authUser.guard'; +import { CustomUserRequest } from 'src/auth/user/interface/customUserReq'; @Controller('route') -@ApiTags('routes') +@ApiTags('route') export class RouteController { constructor(private readonly routeService: RouteService) {} - @Post() - @ApiConsumes('multipart/form-data') - // @UseGuards(AuthUserGuard) - async create(@Body() createRouteDto: CreateRouteDto) { - return await this.routeService.create(); - } - - @Get() - findAll() { - return this.routeService.findAll(); - } + // @Post() + // @ApiConsumes('multipart/form-data') + // // @UseGuards(AuthUserGuard) + // async create(@Body() createRouteDto: CreateRouteDto) { + // return await this.routeService.recommend(); + // } - @Get(':idRoute') - async findOne(@Param('idRoute') idRoute: number) { - return await this.routeService.findOne(idRoute); - } + // @Get(':idRoute') + // async findOne(@Param('idRoute') idRoute: number) { + // return await this.routeService.findOne(idRoute); + // } - @Patch(':id') - update(@Param('id') id: string, @Body() updateRouteDto: UpdateRouteDto) { - return this.routeService.update(+id, updateRouteDto); - } + // @Patch(':id') + // update(@Param('id') id: string, @Body() updateRouteDto: UpdateRouteDto) { + // return this.routeService.update(+id, updateRouteDto); + // } - @Delete(':id') - remove(@Param('id') id: string) { - return this.routeService.remove(+id); - } + // @Delete(':id') + // remove(@Param('id') id: string) { + // return this.routeService.remove(+id); + // } - @Get('recommend') - recommendRoute() { - return this.routeService.recommend(); + @Get('recommend:idTown') + @ApiQuery({ name: 'lang', type: String }) + @ApiParam({ name: 'idTown', type: Number }) + @ApiBearerAuth('jwt') + @UseGuards(AuthUserGuard) + async recommendRoute(@Req() req: CustomUserRequest, @Query('lang') lang: string, @Param('idTown') idTown: number) { + const { email } = req.user; + return await this.routeService.recommend(idTown, email, lang as LANGUAGES); } } diff --git a/backend/src/route/route.module.ts b/backend/src/route/route.module.ts index a9bacc34170c7d85d2420bbeaba7dd237f2f97ab..e3d69a6710116b96e852614f2fac1c3b95ee77da 100644 --- a/backend/src/route/route.module.ts +++ b/backend/src/route/route.module.ts @@ -14,6 +14,11 @@ import { Town } from 'src/town/entities/town.entity'; import { TravelPlace } from 'src/travel-place/entities/travel-place.entity'; import { TravelPlaceService } from 'src/travel-place/travel-place.service'; import { Place } from 'src/place/entities/place.entity'; +import { PlaceService } from 'src/place/place.service'; +import { AvailableDate } from 'src/place/entities/available-date.entity'; +import { PlaceTraduction } from 'src/place/entities/place-traduction.entity'; +import { VisitedService } from 'src/visited/visited.service'; +import { Visited } from 'src/visited/entities/visited.entity'; @Module({ controllers: [RouteController], @@ -25,7 +30,21 @@ import { Place } from 'src/place/entities/place.entity'; EncryptionService, CategoryService, TravelPlaceService, + PlaceService, + VisitedService, + ], + imports: [ + TypeOrmModule.forFeature([ + Route, + User, + Category, + Town, + TravelPlace, + Place, + AvailableDate, + PlaceTraduction, + Visited, + ]), ], - imports: [TypeOrmModule.forFeature([Route, User, Category, Town, TravelPlace, Place])], }) export class RouteModule {} diff --git a/backend/src/route/route.service.ts b/backend/src/route/route.service.ts index c5da153b7586f6ec4d96fec4e4cb87ebe02f8b70..5d359bfe4d63990abbcdd56c1776e4cff3381a87 100644 --- a/backend/src/route/route.service.ts +++ b/backend/src/route/route.service.ts @@ -9,6 +9,12 @@ import { Town } from 'src/town/entities/town.entity'; import { TravelPlaceService } from 'src/travel-place/travel-place.service'; import { TravelPlace } from 'src/travel-place/entities/travel-place.entity'; import { RecommendationsSystem } from './utils/recommendations'; +import { PlaceService } from 'src/place/place.service'; +import { LANGUAGES } from 'src/shared/enum/languages.enum'; +import { VisitedService } from 'src/visited/visited.service'; +import { GetPlaceDto } from 'src/place/dto/get-place.dto'; +import { Visited } from 'src/visited/entities/visited.entity'; +import { RecommendPlace } from './dto/recommend-route.dto'; @Injectable() export class RouteService { @@ -18,10 +24,12 @@ export class RouteService { @InjectRepository(Town) private townRepository: Repository, private readonly travelPlaceService: TravelPlaceService, @InjectDataSource() private dataSource: DataSource, + private readonly placeService: PlaceService, + private readonly visitedService: VisitedService, ) {} - async create() { - const user: User = await this.userRepository.findOneBy({ userId: 1 }); - const town: Town = await this.townRepository.findOneBy({ townId: 1 }); + private async createRoute(idUser: number, idTown: number) { + const user: User = await this.userRepository.findOneBy({ userId: idUser }); + const town: Town = await this.townRepository.findOneBy({ townId: idTown }); await this.routeRepository.save({ user, town, startDate: new Date(), endDate: new Date() }); await this.travelPlaceService.create({ idRoute: 1, @@ -54,10 +62,37 @@ export class RouteService { return `This action removes a #${id} route`; } - recommend() { + async recommend(idTown: number, email: string, language: LANGUAGES) { // Obtener los visitados y los candidatos + const places: GetPlaceDto[] = await this.placeService.findAllByTown(idTown, language); + const visited: Visited[] = await this.visitedService.getVisitedByUser(email); + const placesMapped: RecommendPlace[] = places.map((place) => { + return { + idPlace: place.idPlace, + openAt: place.openAt, + closeAt: place.closeAt, + categories: place.categories.map((category) => category.idCategory), + rating: 0, + }; + }); + + const visitedMapped: RecommendPlace[] = visited.map((visit) => { + return { + idPlace: visit.place.idPlace, + openAt: visit.place.openAt, + closeAt: visit.place.closeAt, + categories: visit.place.categories.map((category) => category.idCategory), + rating: visit.rating, + }; + }); + const system = new RecommendationsSystem(); - // system.recommend(visited, candidates); + const chosen: [number, number][] = system.recommend(visitedMapped, placesMapped); + const placesChooen: GetPlaceDto[] = []; + for (const [index] of chosen) { + placesChooen.push(await this.placeService.findOneAndTradAndAvailable(index, LANGUAGES.EN)); + } + + return placesChooen; } - } diff --git a/backend/src/route/utils/recommendations.ts b/backend/src/route/utils/recommendations.ts index 5835dcec9b7d708a74bcd26f4963dfeb1bd6fec5..88ee597c3583d4c3c41b0d094ffe91ca51859831 100644 --- a/backend/src/route/utils/recommendations.ts +++ b/backend/src/route/utils/recommendations.ts @@ -27,7 +27,7 @@ export class RecommendationsSystem { rankVisited(visited: RecommendPlace[]): Series { const visitedCategories = this.getCategories(visited); const visitedEncoded = this.oneHotEncode(visitedCategories, visited); - const grades = visited.map((place) => place.grade); + const grades = visited.map((place) => place.rating); const dataframe: DataFrame = visitedEncoded; @@ -56,7 +56,7 @@ export class RecommendationsSystem { }, 0); }); - let ranked: [number, number][] = candidates.map((place, i) => [place.idPlace, result[i]]); + const ranked: [number, number][] = candidates.map((place, i) => [place.idPlace, result[i]]); ranked.sort(customSort); return ranked; diff --git a/backend/src/visited/dto/create-visited.dto.ts b/backend/src/visited/dto/create-visited.dto.ts new file mode 100644 index 0000000000000000000000000000000000000000..60f86a2fc4d28ae7e83c2ff68c1f3218d1c1253c --- /dev/null +++ b/backend/src/visited/dto/create-visited.dto.ts @@ -0,0 +1,12 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class CreateVisitedDto { + @ApiProperty() + idPlace: number; + @ApiProperty() + email: string; + @ApiProperty({ type: 'number', description: 'Rating from 1 to 5' }) + rating: number; + @ApiProperty() + date: Date; +} diff --git a/backend/src/visited/dto/update-visited.dto.ts b/backend/src/visited/dto/update-visited.dto.ts new file mode 100644 index 0000000000000000000000000000000000000000..1c9af4900eb7164f77336d3ac43d3f4b999e1405 --- /dev/null +++ b/backend/src/visited/dto/update-visited.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from '@nestjs/swagger'; +import { CreateVisitedDto } from './create-visited.dto'; + +export class UpdateVisitedDto extends PartialType(CreateVisitedDto) {} diff --git a/backend/src/visited/entities/visited.entity.ts b/backend/src/visited/entities/visited.entity.ts new file mode 100644 index 0000000000000000000000000000000000000000..65a92aff1e83330ddd0e9c6c9f4353e4fc64b86e --- /dev/null +++ b/backend/src/visited/entities/visited.entity.ts @@ -0,0 +1,23 @@ +import { Place } from 'src/place/entities/place.entity'; +import { User } from 'src/user/entities/user.entity'; +import { Column, Entity, JoinColumn, ManyToOne, PrimaryGeneratedColumn } from 'typeorm'; + +@Entity() +export class Visited { + @PrimaryGeneratedColumn() + idVisited: number; + + @ManyToOne(() => Place, (place) => place.idPlace, { eager: true }) + @JoinColumn({ name: 'place' }) + place: Place; + + @ManyToOne(() => User, (user) => user.userId) + @JoinColumn({ name: 'user' }) + user: User; + + @Column({ nullable: false }) + rating: number; + + @Column({ nullable: false }) + date: Date; +} diff --git a/backend/src/visited/visited.controller.ts b/backend/src/visited/visited.controller.ts new file mode 100644 index 0000000000000000000000000000000000000000..ea96456ffb89d334d9ef7b89a672e304e8500347 --- /dev/null +++ b/backend/src/visited/visited.controller.ts @@ -0,0 +1,40 @@ +import { Controller, Get, Post, Body, Patch, Param, Delete } from '@nestjs/common'; +import { VisitedService } from './visited.service'; +import { CreateVisitedDto } from './dto/create-visited.dto'; +import { ApiTags } from '@nestjs/swagger'; +// import { UpdateVisitedDto } from './dto/update-visited.dto'; + +@Controller('visited') +@ApiTags('Visited places') +export class VisitedController { + constructor(private readonly visitedService: VisitedService) {} + + @Post() + async create(@Body() createVisitedDto: CreateVisitedDto) { + try { + return await this.visitedService.create(createVisitedDto); + } catch (e) { + return e; + } + } + + @Get() + findAll() { + return this.visitedService.findAll(); + } + + @Get(':id') + findOne(@Param('id') id: string) { + return this.visitedService.findOne(+id); + } + + // @Patch(':id') + // update(@Param('id') id: string, @Body() updateVisitedDto: UpdateVisitedDto) { + // return this.visitedService.update(+id, updateVisitedDto); + // } + + @Delete(':id') + remove(@Param('id') id: string) { + return this.visitedService.remove(+id); + } +} diff --git a/backend/src/visited/visited.module.ts b/backend/src/visited/visited.module.ts new file mode 100644 index 0000000000000000000000000000000000000000..022aaf567fe62aa23e1b014b9b34e96ceefa9ac9 --- /dev/null +++ b/backend/src/visited/visited.module.ts @@ -0,0 +1,21 @@ +import { Module } from '@nestjs/common'; +import { VisitedService } from './visited.service'; +import { VisitedController } from './visited.controller'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { UserService } from 'src/user/user.service'; +import { PlaceService } from 'src/place/place.service'; +import { User } from 'src/user/entities/user.entity'; +import { Place } from 'src/place/entities/place.entity'; +import { Visited } from './entities/visited.entity'; +import { CategoryService } from 'src/category/category.service'; +import { Category } from 'src/category/entities/category.entity'; +import { AvailableDate } from 'src/place/entities/available-date.entity'; +import { PlaceTraduction } from 'src/place/entities/place-traduction.entity'; +import { Town } from 'src/town/entities/town.entity'; + +@Module({ + controllers: [VisitedController], + providers: [VisitedService, UserService, PlaceService, CategoryService], + imports: [TypeOrmModule.forFeature([User, Place, Visited, Category, AvailableDate, PlaceTraduction, Town])], +}) +export class VisitedModule {} diff --git a/backend/src/visited/visited.service.ts b/backend/src/visited/visited.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..c4a4f6869fba7cd4e56ed46578687a7aeb5c6259 --- /dev/null +++ b/backend/src/visited/visited.service.ts @@ -0,0 +1,50 @@ +import { BadRequestException, Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Visited } from './entities/visited.entity'; +import { Repository } from 'typeorm'; +import { User } from 'src/user/entities/user.entity'; +import { UserService } from 'src/user/user.service'; +import { CreateVisitedDto } from './dto/create-visited.dto'; +import { Place } from 'src/place/entities/place.entity'; +import { PlaceService } from 'src/place/place.service'; +import { UpdateVisitedDto } from './dto/update-visited.dto'; + +@Injectable() +export class VisitedService { + constructor( + @InjectRepository(Visited) private visitedRepository: Repository, + private readonly userService: UserService, + private readonly placeService: PlaceService, + ) {} + + async create(createVisitedDto: CreateVisitedDto) { + const place: Place = await this.placeService.findOne(createVisitedDto.idPlace); + const user: User = await this.userService.findOne(createVisitedDto.email); + if (!place || !user) throw new BadRequestException('Place or user not found'); + await this.visitedRepository.save({ place, user, rating: createVisitedDto.rating, date: createVisitedDto.date }); + } + + async getVisitedByUser(email: string): Promise { + const user: User = await this.userService.findOne(email); + return this.visitedRepository.find({ + where: { user }, + relations: ['place', 'place.categories'], + }); + } + + findAll() { + return `This action returns all visited`; + } + + findOne(id: number) { + return `This action returns a #${id} visited`; + } + + // update(id: number, updateVisitedDto: UpdateVisitedDto) { + // return `This action updates a #${id} visited`; + // } + + remove(id: number) { + return `This action removes a #${id} visited`; + } +}