diff --git a/backend/package-lock.json b/backend/package-lock.json index ff7ea91d99cf333eafc91196ab31a981c3910456..30e373bf2c516580af16ea2e337b809996b3f40e 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -19,8 +19,9 @@ "@nestjs/swagger": "^7.3.0", "@nestjs/typeorm": "^10.0.2", "bcrypt": "^5.1.1", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.1", "dotenv": "^16.4.5", - "ip": "^2.0.1", "mysql2": "^3.9.2", "network": "^0.7.0", "reflect-metadata": "^0.2.0", @@ -2709,6 +2710,11 @@ "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==" }, + "node_modules/@types/validator": { + "version": "13.12.0", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.12.0.tgz", + "integrity": "sha512-nH45Lk7oPIJ1RVOF6JgFI6Dy0QpHEzq4QecZhvguxYPDwT8c93prCMqAtiIttm39voZ+DDR+qkNnMpJmMBRqag==" + }, "node_modules/@types/yargs": { "version": "17.0.32", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", @@ -3872,6 +3878,21 @@ "integrity": "sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==", "dev": true }, + "node_modules/class-transformer": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz", + "integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==" + }, + "node_modules/class-validator": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.1.tgz", + "integrity": "sha512-2VEG9JICxIqTpoK1eMzZqaV+u/EiwEJkMGzTrZf6sU/fwsnOITVgYJ8yojSy6CaXtO9V0Cc6ZQZ8h8m4UBuLwQ==", + "dependencies": { + "@types/validator": "^13.11.8", + "libphonenumber-js": "^1.10.53", + "validator": "^13.9.0" + } + }, "node_modules/cli-cursor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", @@ -6082,11 +6103,6 @@ "node": ">= 0.10" } }, - "node_modules/ip": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.1.tgz", - "integrity": "sha512-lJUL9imLTNi1ZfXT+DU6rBBdbiKGBuay9B6xGSPVjUeQwaH1RIGqef8RZkUtHioLmSNpPR5M4HVKJGm1j8FWVQ==" - }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -7169,6 +7185,11 @@ "node": ">= 0.8.0" } }, + "node_modules/libphonenumber-js": { + "version": "1.11.4", + "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.11.4.tgz", + "integrity": "sha512-F/R50HQuWWYcmU/esP5jrH5LiWYaN7DpN0a/99U8+mnGGtnx8kmRE+649dQh3v+CowXXZc8vpkf5AmYkO0AQ7Q==" + }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", @@ -10066,6 +10087,14 @@ "node": ">=10.12.0" } }, + "node_modules/validator": { + "version": "13.12.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.12.0.tgz", + "integrity": "sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", diff --git a/backend/package.json b/backend/package.json index 541598de2e0ef9ff2e6e0941ba4d5307725fb6bf..4b33bcaba3f58b4ea1241049592b721fe1dab06c 100644 --- a/backend/package.json +++ b/backend/package.json @@ -30,6 +30,8 @@ "@nestjs/swagger": "^7.3.0", "@nestjs/typeorm": "^10.0.2", "bcrypt": "^5.1.1", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.1", "dotenv": "^16.4.5", "mysql2": "^3.9.2", "network": "^0.7.0", diff --git a/backend/src/admin/admin.service.ts b/backend/src/admin/admin.service.ts index 793c06a5af237597b185aa0d1a401f64e3138dcf..65c9a37712d9bf7d253c8f463226da095728d14e 100644 --- a/backend/src/admin/admin.service.ts +++ b/backend/src/admin/admin.service.ts @@ -4,6 +4,7 @@ import { Admin } from './entities/admin.entity'; import { Repository } from 'typeorm'; import { InjectRepository } from '@nestjs/typeorm'; import { Town } from 'src/town/entities/town.entity'; +import { ADMIN_ROLE } from 'src/shared/enum/admin-role.enum'; @Injectable() export class AdminService { @@ -12,7 +13,10 @@ export class AdminService { @InjectRepository(Town) private townRepository: Repository, ) {} async create(createAdminDto: CreateAdminDto) { - const town = await this.townRepository.findOneByOrFail({ townId: createAdminDto.idTown }); + let town: Town = null; + if (createAdminDto.role == ADMIN_ROLE.ADMIN) { + town = await this.townRepository.findOneByOrFail({ townId: createAdminDto.idTown }); + } await this.adminRepository.insert({ ...createAdminDto, idTown: town }); } diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index 85d00ffe452f0b8491cb5267222f2d806f385493..6567c8b2ae61cbe884c708aafe07e7cd6bdb44b3 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -17,8 +17,6 @@ import { join } from 'path'; import { Town } from './town/entities/town.entity'; import { TownModule } from './town/town.module'; import { TownTraduction } from './town/entities/town-traduction.entity'; -import { APP_GUARD } from '@nestjs/core'; -import { AuthAdminGuard } from './auth/admin/authAdmin.guard'; import { PlaceModule } from './place/place.module'; import { Place } from './place/entities/place.entity'; import { PointOfInterestModule } from './pointOfInterest/PointOfInterest.module'; @@ -26,6 +24,8 @@ import { PointOfInterest } from './pointOfInterest/entities/PointOfInterest.enti import { AvailableDate } from './place/entities/available-date.entity'; import { PointOfInterestTraduction } from './pointOfInterest/entities/PointOfInterestTraduction.entity'; import { PlaceTraduction } from './place/entities/place-traduction.entity'; +import { CategoryModule } from './category/category.module'; +import { Category } from './category/entities/category.entity'; @Module({ imports: [ @@ -47,9 +47,10 @@ import { PlaceTraduction } from './place/entities/place-traduction.entity'; AvailableDate, PointOfInterestTraduction, PlaceTraduction, + Category, ], synchronize: DbConstants.DB_SYNC, - logging: true, + logging: false, }), AuthAdminModule, AdminModule, @@ -63,6 +64,7 @@ import { PlaceTraduction } from './place/entities/place-traduction.entity'; rootPath: join(__dirname, '..', 'static'), }), PointOfInterestModule, + CategoryModule, ], controllers: [AppController], providers: [AppService, DatabaseSeederModule], diff --git a/backend/src/auth/user/authUser.module.ts b/backend/src/auth/user/authUser.module.ts index 22ac1b095f28932baa63f08b6b2b57b6c2370453..60fb0b10ac8580120378d2c3a4f36fdecac409e1 100644 --- a/backend/src/auth/user/authUser.module.ts +++ b/backend/src/auth/user/authUser.module.ts @@ -6,9 +6,11 @@ import { AuthUserController } from './authUsercontroller'; import { UserService } from 'src/user/user.service'; import { User } from 'src/user/entities/user.entity'; import { AuthUserService } from './authUserservice'; +import { Category } from 'src/category/entities/category.entity'; +import { CategoryService } from 'src/category/category.service'; @Module({ controllers: [AuthUserController], - providers: [UserService, JwtService, EncryptionService, AuthUserService], - imports: [TypeOrmModule.forFeature([User])], + providers: [UserService, JwtService, EncryptionService, AuthUserService, CategoryService], + imports: [TypeOrmModule.forFeature([User, Category])], }) export class AuthUserModule {} diff --git a/backend/src/category/category.controller.ts b/backend/src/category/category.controller.ts new file mode 100644 index 0000000000000000000000000000000000000000..c5395cfcc5639e5a0fd0c5ebc9daf89546485e59 --- /dev/null +++ b/backend/src/category/category.controller.ts @@ -0,0 +1,43 @@ +import { Controller, Get, Post, Body, Param, Delete, UseGuards } from '@nestjs/common'; +import { CategoryService } from './category.service'; +import { AuthAdminGuard } from 'src/auth/admin/authAdmin.guard'; +import { Roles } from 'src/auth/role.decorator'; +import { SUPERADMIN_ROLES } from 'src/shared/enum/admin-role.enum'; +import { ApiBearerAuth, ApiBody, ApiTags } from '@nestjs/swagger'; +import { CreateCategoryReqDto } from './dto/create-category-req.dto'; +import { LANGUAGES } from 'src/shared/enum/languages.enum'; + +@Controller('category') +@ApiTags('Category') +export class CategoryController { + constructor(private readonly categoryService: CategoryService) {} + + @UseGuards(AuthAdminGuard) + @Roles(SUPERADMIN_ROLES) + @ApiBearerAuth('jwt') + @ApiBody({ type: CreateCategoryReqDto }) + @Post() + create(@Body() createCategoryDto: CreateCategoryReqDto) { + this.categoryService.create(createCategoryDto); + return { message: 'Category created successfully' }; + } + + @Get(':lang') + findAll(@Param('lang') lang: LANGUAGES) { + return this.categoryService.findAll(lang); + } + + // @Patch(':id') + // update(@Param('id') id: string, @Body() updateCategoryDto: UpdateCategoryDto) { + // return this.categoryService.update(+id, updateCategoryDto); + // } + + @UseGuards(AuthAdminGuard) + @Roles(SUPERADMIN_ROLES) + @ApiBearerAuth('jwt') + @Delete(':id') + remove(@Param('id') id: string) { + this.categoryService.remove(+id); + return { message: 'Category deleted successfully' }; + } +} diff --git a/backend/src/category/category.module.ts b/backend/src/category/category.module.ts new file mode 100644 index 0000000000000000000000000000000000000000..403cc8879a3822cb9dfde37558c35ae21196fb7a --- /dev/null +++ b/backend/src/category/category.module.ts @@ -0,0 +1,19 @@ +import { Module } from '@nestjs/common'; +import { CategoryService } from './category.service'; +import { CategoryController } from './category.controller'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { Category } from './entities/category.entity'; +import { AuthAdminService } from 'src/auth/admin/authAdmin.service'; +import { JwtService } from '@nestjs/jwt'; +import { EncryptionService } from 'src/auth/encryption/encryption.service'; +import { AdminService } from 'src/admin/admin.service'; +import { Admin } from 'src/admin/entities/admin.entity'; +import { Town } from 'src/town/entities/town.entity'; + +@Module({ + controllers: [CategoryController], + providers: [CategoryService, AuthAdminService, JwtService, EncryptionService, AdminService], + imports: [TypeOrmModule.forFeature([Category, Admin, Town])], + exports: [CategoryService], +}) +export class CategoryModule {} diff --git a/backend/src/category/category.service.ts b/backend/src/category/category.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..c4f75479ee5c5c961d44be6b240c19405c2eaa5b --- /dev/null +++ b/backend/src/category/category.service.ts @@ -0,0 +1,38 @@ +import { Injectable } from '@nestjs/common'; +import { CreateCategoryDto } from './dto/create-category.dto'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Category } from './entities/category.entity'; +import { LANGUAGES } from 'src/shared/enum/languages.enum'; +import { CreateCategoryReqDto } from './dto/create-category-req.dto'; + +@Injectable() +export class CategoryService { + constructor(@InjectRepository(Category) private categoryRepository) {} + + async create(createCategoryDto: CreateCategoryReqDto): Promise { + const categoriyEN: CreateCategoryDto = { + name: createCategoryDto.nameEN, + language: LANGUAGES.EN, + }; + + const insertedId: number = (await this.categoryRepository.insert(categoriyEN)).raw.insertId; + + await this.categoryRepository.insert({ + idCategory: insertedId, + name: createCategoryDto.nameES, + language: LANGUAGES.ES, + }); + } + + async findAll(lang: string) { + return await this.categoryRepository.find({ where: { language: lang }, select: ['idCategory', 'name'] }); + } + + async remove(idCategory: number) { + await this.categoryRepository.delete({ idCategory }); + } + + async findOne(idCategory: number): Promise { + return await this.categoryRepository.findOne({ where: { idCategory } }); + } +} diff --git a/backend/src/category/dto/create-category-req.dto.ts b/backend/src/category/dto/create-category-req.dto.ts new file mode 100644 index 0000000000000000000000000000000000000000..3f1e6b18aa20c3c367a45e114d149c04e83defe4 --- /dev/null +++ b/backend/src/category/dto/create-category-req.dto.ts @@ -0,0 +1,8 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class CreateCategoryReqDto { + @ApiProperty() + nameES: string; + @ApiProperty() + nameEN: string; +} diff --git a/backend/src/category/dto/create-category.dto.ts b/backend/src/category/dto/create-category.dto.ts new file mode 100644 index 0000000000000000000000000000000000000000..eab17f0db39f8257ba220d733b9e4853f6fce73c --- /dev/null +++ b/backend/src/category/dto/create-category.dto.ts @@ -0,0 +1,7 @@ +import { LANGUAGES } from 'src/shared/enum/languages.enum'; + +export class CreateCategoryDto { + idCategory?: number; + language: LANGUAGES; + name: string; +} diff --git a/backend/src/category/dto/update-category.dto.ts b/backend/src/category/dto/update-category.dto.ts new file mode 100644 index 0000000000000000000000000000000000000000..d713b9b900cd1d580baedbf636a1dd8470c04296 --- /dev/null +++ b/backend/src/category/dto/update-category.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from '@nestjs/swagger'; +import { CreateCategoryDto } from './create-category.dto'; + +export class UpdateCategoryDto extends PartialType(CreateCategoryDto) {} diff --git a/backend/src/category/entities/category.entity.ts b/backend/src/category/entities/category.entity.ts new file mode 100644 index 0000000000000000000000000000000000000000..3236e8a276a9975ce3cc0859c3e77ab32940b0fe --- /dev/null +++ b/backend/src/category/entities/category.entity.ts @@ -0,0 +1,13 @@ +import { Column, Entity, PrimaryColumn, PrimaryGeneratedColumn } from 'typeorm'; + +@Entity() +export class Category { + @PrimaryGeneratedColumn() + idCategory: number; + + @PrimaryColumn({ nullable: false, primary: true }) + language: string; + + @Column({ nullable: false }) + name: string; +} diff --git a/backend/src/database-seeder/database-seeder.module.ts b/backend/src/database-seeder/database-seeder.module.ts index 80f8867dbd0b0a02310f0fb1f16371b3eb4b95e0..f9c5fb36ef36e39196bac9b44cb5e71255b6ef13 100644 --- a/backend/src/database-seeder/database-seeder.module.ts +++ b/backend/src/database-seeder/database-seeder.module.ts @@ -19,6 +19,8 @@ import { PointOfInterest } from 'src/pointOfInterest/entities/PointOfInterest.en import { AvailableDate } from 'src/place/entities/available-date.entity'; import { PlaceTraduction } from 'src/place/entities/place-traduction.entity'; import { PointOfInterestTraduction } from 'src/pointOfInterest/entities/PointOfInterestTraduction.entity'; +import { CategoryService } from 'src/category/category.service'; +import { Category } from 'src/category/entities/category.entity'; @Module({ providers: [ @@ -31,6 +33,7 @@ import { PointOfInterestTraduction } from 'src/pointOfInterest/entities/PointOfI TownService, PointOfInterestService, PlaceService, + CategoryService, ], imports: [ TypeOrmModule.forFeature([ @@ -44,6 +47,7 @@ import { PointOfInterestTraduction } from 'src/pointOfInterest/entities/PointOfI AvailableDate, PlaceTraduction, PointOfInterestTraduction, + Category, ]), ], }) diff --git a/backend/src/database-seeder/database-seeder.service.ts b/backend/src/database-seeder/database-seeder.service.ts index 6e678340c3280fdc139385a93b1452a69162b2fa..522177fc5ff96cfa311b59b79c02f880492d0e39 100644 --- a/backend/src/database-seeder/database-seeder.service.ts +++ b/backend/src/database-seeder/database-seeder.service.ts @@ -10,8 +10,8 @@ import { UserStatus } from 'src/shared/enum/user-status.enum'; import { AuthAdminService } from 'src/auth/admin/authAdmin.service'; import { TownService } from 'src/town/town.service'; import { CreateTownDto } from 'src/town/dto/create-town.dto'; -import { PointOfInterestService } from 'src/pointOfInterest/PointOfInterest.service'; -import { PlaceService } from 'src/place/place.service'; +import { LANGUAGES } from 'src/shared/enum/languages.enum'; +import { Category } from 'src/category/entities/category.entity'; @Injectable() export class DatabaseSeederService implements OnModuleInit { @@ -20,8 +20,7 @@ export class DatabaseSeederService implements OnModuleInit { private readonly stateService: StateService, private readonly authAdminService: AuthAdminService, private readonly townService: TownService, - private readonly activityService: PointOfInterestService, - private readonly placeService: PlaceService, + @InjectRepository(Category) private categoryRepo: Repository, ) {} async insertStates() { @@ -93,9 +92,35 @@ export class DatabaseSeederService implements OnModuleInit { await this.townService.create(town); } + async insertCategories() { + const categoriesES = [ + { idCategory: 1, language: LANGUAGES.ES, name: 'Arquitectura' }, + { idCategory: 2, language: LANGUAGES.ES, name: 'Comida' }, + ]; + const categoriesEN = [ + { idCategory: 1, language: LANGUAGES.EN, name: 'Architecture' }, + { idCategory: 2, language: LANGUAGES.EN, name: 'Food' }, + ]; + for (let i = 0; i < categoriesES.length; i++) { + await this.categoryRepo.upsert( + [{ idCategory: categoriesES[i].idCategory, language: LANGUAGES.ES, name: categoriesES[i].name }], + { + conflictPaths: ['idCategory', 'language'], + }, + ); + await this.categoryRepo.upsert( + [{ idCategory: categoriesES[i].idCategory, language: LANGUAGES.EN, name: categoriesEN[i].name }], + { + conflictPaths: ['idCategory', 'language'], + }, + ); + } + } + async onModuleInit() { await this.insertStates(); - await this.insertSuperAdmin(); await this.insertTowns(); + await this.insertSuperAdmin(); + await this.insertCategories(); } } diff --git a/backend/src/main.ts b/backend/src/main.ts index 733878dbb782afba76661ae84c079bf3365b12d7..08ac9ed60c093eb7807e47cd68bf60f2ecc6a4cd 100644 --- a/backend/src/main.ts +++ b/backend/src/main.ts @@ -2,9 +2,15 @@ import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger'; import { ServerConstants } from './constants/server.contants'; +import { ValidationPipe } from '@nestjs/common'; async function bootstrap() { const app = await NestFactory.create(AppModule); + app.useGlobalPipes( + new ValidationPipe({ + transform: true, + }), + ); const config = new DocumentBuilder() .setTitle('Pueblos Magicos API') .setVersion('1.0') diff --git a/backend/src/place/dto/create-date.dto.ts b/backend/src/place/dto/create-date.dto.ts index a311bb99bfd223d060c9a0e87b9d936dc55e5d76..7a127a243915f0e125cbd1439ef6da1ca3d3d7a6 100644 --- a/backend/src/place/dto/create-date.dto.ts +++ b/backend/src/place/dto/create-date.dto.ts @@ -1,4 +1,5 @@ import { ApiProperty } from '@nestjs/swagger'; +import { Place } from '../entities/place.entity'; export class CreateDateDto { @ApiProperty() @@ -7,5 +8,5 @@ export class CreateDateDto { @ApiProperty() endDate: Date; - idPlace: number; + idPlace: Place; } diff --git a/backend/src/place/dto/create-place-date.dto.ts b/backend/src/place/dto/create-place-date.dto.ts index 7e3adcc5442852ce4b40815ed5f28f24979a6124..a8162052e9524a76ac16db4307cdb9adef051fbe 100644 --- a/backend/src/place/dto/create-place-date.dto.ts +++ b/backend/src/place/dto/create-place-date.dto.ts @@ -1,5 +1,7 @@ import { ApiProperty } from '@nestjs/swagger'; import { Available } from 'src/pointOfInterest/enum/available.enum'; +import { Transform } from 'class-transformer'; +import { IsArray } from 'class-validator'; export class CreatePlaceDateTradDto { @ApiProperty({ enum: Available, enumName: 'Available' }) @@ -11,6 +13,11 @@ export class CreatePlaceDateTradDto { @ApiProperty() name: string; + @ApiProperty({ type: String, description: 'Comma-separated list of category IDs', example: '1,2,3' }) + @IsArray({}) + @Transform(({ value }) => value.split(',').map(Number)) + categoriesId: number[]; + @ApiProperty() descriptionES: string; @ApiProperty() diff --git a/backend/src/place/dto/create-place.dto.ts b/backend/src/place/dto/create-place.dto.ts index b37e0f41e9cf46cabbe82970488b7545ad36a28c..63fd93309fc81b69bb70b91fd078e1db4ff3e160 100644 --- a/backend/src/place/dto/create-place.dto.ts +++ b/backend/src/place/dto/create-place.dto.ts @@ -1,3 +1,4 @@ +import { Category } from 'src/category/entities/category.entity'; import { Available } from 'src/pointOfInterest/enum/available.enum'; import { Town } from 'src/town/entities/town.entity'; @@ -5,9 +6,10 @@ export class CreatePlaceDto { available: Available; name: string; imageName: string; - idTown: Town; + town: Town; latitude: number; longitude: number; openAt: number; closeAt: number; + categories: Category[]; } diff --git a/backend/src/place/dto/get-place.dto.ts b/backend/src/place/dto/get-place.dto.ts index 4625b1c510463fba204f33ed5a747c4f8e961009..ae110d5575ac2a9b2ac410263499e931f37c34c8 100644 --- a/backend/src/place/dto/get-place.dto.ts +++ b/backend/src/place/dto/get-place.dto.ts @@ -1,17 +1,30 @@ -import { Available } from 'src/pointOfInterest/enum/available.enum'; -import { Place } from '../entities/place.entity'; +import { ApiProperty } from '@nestjs/swagger'; export class GetPlaceDto { + @ApiProperty() idTown: number; - idPlace: Place; - available: Available; + @ApiProperty() + idPlace: number; + @ApiProperty() name: string; + @ApiProperty() description: string; + @ApiProperty() + available: string; + @ApiProperty() imageName: string; + @ApiProperty() latitude: number; + @ApiProperty() longitude: number; + @ApiProperty() openAt: number; + @ApiProperty() closeAt: number; + @ApiProperty() startDate: Date; + @ApiProperty() endDate: Date; + @ApiProperty() + categories: any[]; } diff --git a/backend/src/place/entities/place-traduction.entity.ts b/backend/src/place/entities/place-traduction.entity.ts index 3c29b7dec07bc99147088c1609b981236e27a863..5804fa470f310041b62f70365484b40dab0086f5 100644 --- a/backend/src/place/entities/place-traduction.entity.ts +++ b/backend/src/place/entities/place-traduction.entity.ts @@ -9,7 +9,7 @@ export class PlaceTraduction { @PrimaryColumn({ name: 'idPlace', type: Number }) @ManyToOne(() => Place, (place) => place.availableDates, { nullable: false }) - idPlace: Place; + idPlace: number; @Column() description: string; diff --git a/backend/src/place/entities/place.entity.ts b/backend/src/place/entities/place.entity.ts index 3586960997fda6ad1faa9879bf2b93126226ed7c..00412dccb1e8072b1164a397dd1870884676d481 100644 --- a/backend/src/place/entities/place.entity.ts +++ b/backend/src/place/entities/place.entity.ts @@ -1,8 +1,19 @@ import { PointOfInterest } from 'src/pointOfInterest/entities/PointOfInterest.entity'; import { Available } from 'src/pointOfInterest/enum/available.enum'; import { Town } from 'src/town/entities/town.entity'; -import { Column, Entity, JoinColumn, ManyToOne, OneToMany, PrimaryGeneratedColumn } from 'typeorm'; +import { + Column, + Entity, + JoinColumn, + JoinTable, + ManyToMany, + ManyToOne, + OneToMany, + PrimaryGeneratedColumn, +} from 'typeorm'; import { AvailableDate } from './available-date.entity'; +import { Category } from 'src/category/entities/category.entity'; +import { PlaceTraduction } from './place-traduction.entity'; @Entity() export class Place { @@ -11,7 +22,7 @@ export class Place { @JoinColumn({ name: 'idTown' }) @ManyToOne(() => Town, (town) => town.townId, { nullable: false }) - idTown: Town; + town: Town; @OneToMany(() => PointOfInterest, (point) => point.idPoint) points: PointOfInterest[]; @@ -19,6 +30,13 @@ export class Place { @OneToMany(() => AvailableDate, (available) => available.idPlace) availableDates: AvailableDate[]; + @ManyToMany(() => Category) + @JoinTable() + categories: Category[]; + + @OneToMany(() => PlaceTraduction, (placeTraduction) => placeTraduction.idPlace) + placeTraduction: PlaceTraduction[]; + @Column() available: Available; diff --git a/backend/src/place/place.controller.ts b/backend/src/place/place.controller.ts index 4e26dcfbca14e25f4b97b9d59443990c1dcd0705..b98b3ae8447043bd4718649dabc4f0298abc29ad 100644 --- a/backend/src/place/place.controller.ts +++ b/backend/src/place/place.controller.ts @@ -14,7 +14,7 @@ import { } from '@nestjs/common'; import { PlaceService } from './place.service'; import { CreatePlaceDateTradDto } from './dto/create-place-date.dto'; -import { ApiBearerAuth, ApiBody, ApiConsumes, ApiParam, ApiQuery, ApiTags } from '@nestjs/swagger'; +import { ApiBearerAuth, ApiBody, ApiConsumes, ApiParam, ApiQuery, ApiResponse, ApiTags } from '@nestjs/swagger'; import { Roles } from 'src/auth/role.decorator'; import { ALL_ROLES } from 'src/shared/enum/admin-role.enum'; import { fileInterceptor } from 'src/shared/interceptors/file-save.interceptor'; @@ -23,6 +23,7 @@ import { LANGUAGES } from 'src/shared/enum/languages.enum'; import { CustomAdminRequest } from 'src/auth/admin/interface/customAdminReq'; import { UpdatePlaceReqDto } from './dto/update-place.req.dto'; import { AuthAdminGuard } from 'src/auth/admin/authAdmin.guard'; +import { GetPlaceDto } from './dto/get-place.dto'; @Controller('place') @ApiTags('Place') @@ -31,6 +32,7 @@ export class PlaceController { @ApiBody({ type: CreatePlaceDateTradDto }) @ApiConsumes('multipart/form-data') + @ApiResponse({ type: GetPlaceDto, status: 201, description: 'Created' }) @UseGuards(AuthAdminGuard) @Roles([ALL_ROLES.ADMIN]) @ApiBearerAuth('jwt') @@ -46,7 +48,8 @@ export class PlaceController { throw new UnauthorizedException('This is not your assigned town'); } createPlaceDto.image = file; - return await this.placeService.create(createPlaceDto); + await this.placeService.create(createPlaceDto); + return { message: 'Place created successfully' }; } catch (e) { throw e; } @@ -83,13 +86,12 @@ export class PlaceController { @Req() req: CustomAdminRequest, ) { try { - const place = await this.placeService.findOne(idPlace); - console.log({ place }); if (req.admin.idTown.townId != updatePlaceReqDto.idTown) { throw new UnauthorizedException('This is not your assigned town'); } updatePlaceReqDto.image = file; - return await this.placeService.update(idPlace, updatePlaceReqDto); + await this.placeService.update(idPlace, updatePlaceReqDto); + return { message: 'Place updated successfully' }; } catch (e) { throw e; } diff --git a/backend/src/place/place.service.ts b/backend/src/place/place.service.ts index 214c40d0a944e0b4bc1fe3ad5e58c82084aac869..e0f8d5922b6d25d3da37d8102e3f0e71d90e2af0 100644 --- a/backend/src/place/place.service.ts +++ b/backend/src/place/place.service.ts @@ -1,4 +1,4 @@ -import { BadRequestException, Injectable } from '@nestjs/common'; +import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common'; import { CreatePlaceDateTradDto } from './dto/create-place-date.dto'; import { InjectDataSource, InjectRepository } from '@nestjs/typeorm'; import { Place } from './entities/place.entity'; @@ -13,6 +13,7 @@ import { GetPlaceDto } from './dto/get-place.dto'; import { ServerConstants } from 'src/constants/server.contants'; import { Available } from 'src/pointOfInterest/enum/available.enum'; import { UpdatePlaceReqDto } from './dto/update-place.req.dto'; +import { Category } from 'src/category/entities/category.entity'; @Injectable() export class PlaceService { @@ -26,35 +27,46 @@ export class PlaceService { async create(createPlaceDto: CreatePlaceDateTradDto) { const town: Town = await this.townRepository.findOneBy({ townId: createPlaceDto.idTown }); + const categories: Category[] = []; + for (const categoryId of createPlaceDto.categoriesId) { + const categoryEN: Category = await this.dataSource + .getRepository(Category) + .findOneByOrFail({ idCategory: categoryId }); + categoryEN.language = LANGUAGES.EN; + const categoryES: Category = { ...categoryEN, language: LANGUAGES.ES }; + categories.push(categoryEN); + categories.push(categoryES); + } const createPlace: CreatePlaceDto = { available: createPlaceDto.available, closeAt: createPlaceDto.closeAt, latitude: createPlaceDto.latitude, longitude: createPlaceDto.longitude, - idTown: town, + town: town, name: createPlaceDto.name, openAt: createPlaceDto.openAt, imageName: createPlaceDto.image.filename, + categories: categories, }; if (!town) { throw new BadRequestException('Town not found'); } - const insertedId = (await this.placeRepository.insert({ ...createPlace })).raw.insertId; + const insertedPlace = await this.placeRepository.save({ ...createPlace }); const createDate: CreateDateDto = { endDate: createPlaceDto.endDate, startDate: createPlaceDto.startDate, - idPlace: insertedId, + idPlace: insertedPlace, }; const createTradEn: PlaceTraduction = { language: LANGUAGES.EN, - idPlace: insertedId, + idPlace: insertedPlace.idPlace, description: createPlaceDto.descriptionEN, }; const createTradEs: PlaceTraduction = { language: LANGUAGES.ES, - idPlace: insertedId, + idPlace: insertedPlace.idPlace, description: createPlaceDto.descriptionES, }; @@ -62,48 +74,37 @@ export class PlaceService { await this.placeTraductionRepository.insert(createTradEs); await this.placeTraductionRepository.insert(createTradEn); if (createPlaceDto.available === Available.CUSTOM) { - await this.availableDateRepository.insert({ ...createDate, idPlace: insertedId }); + await this.availableDateRepository.insert({ ...createDate, idPlace: insertedPlace }); } } async findAllByTown(idTown: number, lang: LANGUAGES) { - const res: GetPlaceDto[] = await this.dataSource - .getRepository(PlaceTraduction) - .createQueryBuilder('placeTrad') - .leftJoin('placeTrad.idPlace', 'place') - .leftJoin('place.availableDates', 'availableDate') - .select([ - 'place.idPlace AS idPlace', - 'place.name AS name', - 'place.imageName AS imageName', - 'placeTrad.language AS language', - 'placeTrad.description AS description', - 'place.openAt AS openAt', - 'place.closeAt AS closeAt', - 'availableDate.startDate AS startDate', - 'availableDate.endDate AS endDate', - 'place.available AS available', - 'place.idTown AS idTown', - 'place.latitude AS latitude', - 'place.longitude AS longitude', - ]) + const res: any[] = await this.dataSource + .getRepository(Place) + .createQueryBuilder('place') + .leftJoinAndSelect('place.availableDates', 'availableDate') + .leftJoinAndSelect('place.categories', 'category') + .leftJoinAndSelect('place.placeTraduction', 'placeTrad') + .leftJoinAndSelect('place.town', 'town') .where('place.idTown = :idTown', { idTown: idTown }) .andWhere('placeTrad.language = :language', { language: lang }) - .getRawMany(); - const places: GetPlaceDto[] = res.map((place) => { + .andWhere('category.language = :language', { language: lang }) + .getMany(); + const places: GetPlaceDto[] = res.map((place): GetPlaceDto => { return { - idTown: place.idTown, + idTown: place.town.townId, idPlace: place.idPlace, available: place.available, - description: place.description, latitude: place.latitude, longitude: place.longitude, imageName: `${ServerConstants.HOST}/places/${place.imageName}`, name: place.name, openAt: place.openAt, closeAt: place.closeAt, - startDate: place.startDate, - endDate: place.endDate, + startDate: place.availableDates[0]?.startDate || null, + endDate: place.availableDates[0]?.endDate || null, + categories: place.categories, + description: place.placeTraduction[0].description, }; }); return places; @@ -114,60 +115,71 @@ export class PlaceService { } async findOneAndTradAndAvailable(idPlace: number, lang: LANGUAGES) { - const place: GetPlaceDto = await this.dataSource - .getRepository(PlaceTraduction) - .createQueryBuilder('placeTrad') - .leftJoin('placeTrad.idPlace', 'place') - .leftJoin('place.availableDates', 'availableDate') - .select([ - 'place.idPlace AS idPlace', - 'place.name AS name', - 'place.imageName AS imageName', - 'placeTrad.language AS language', - 'placeTrad.description AS description', - 'place.latitude AS latitude', - 'place.longitude AS longitude', - 'place.openAt AS openAt', - 'place.closeAt AS closeAt', - 'availableDate.startDate AS startDate', - 'availableDate.endDate AS endDate', - 'place.idTown AS idTown', - 'place.available AS available', - ]) - .where('place.idPlace = :idPlace', { idPlace: idPlace }) + const place: any = await this.dataSource + .getRepository(Place) + .createQueryBuilder('place') + .leftJoinAndSelect('place.availableDates', 'availableDate') + .leftJoinAndSelect('place.categories', 'category') + .leftJoinAndSelect('place.placeTraduction', 'placeTrad') + .leftJoinAndSelect('place.town', 'town') + .where('place.idPlace = :idPlace', { idPlace }) .andWhere('placeTrad.language = :language', { language: lang }) - .getRawOne(); - - return { - idTown: place.idTown, + .andWhere('category.language = :language', { language: lang }) + .getOne(); + const res: GetPlaceDto = { + idTown: place.town.townId, idPlace: place.idPlace, available: place.available, - description: place.description, latitude: place.latitude, longitude: place.longitude, imageName: `${ServerConstants.HOST}/places/${place.imageName}`, name: place.name, openAt: place.openAt, closeAt: place.closeAt, - startDate: place.startDate, - endDate: place.endDate, + startDate: place.availableDates[0]?.startDate, + endDate: place.availableDates[0]?.endDate, + categories: place.categories, + description: place.placeTraduction[0].description, }; + return res; } async update(idPlace: number, updatePlaceReqDto: UpdatePlaceReqDto) { - const place: Place = await this.placeRepository.findOneBy({ idPlace }); - const createPlaceDto: CreatePlaceDto = { - available: updatePlaceReqDto.available, - closeAt: updatePlaceReqDto.closeAt, - latitude: updatePlaceReqDto.latitude, - longitude: updatePlaceReqDto.longitude, - name: updatePlaceReqDto.name, - openAt: updatePlaceReqDto.openAt, - imageName: updatePlaceReqDto.image.filename, - idTown: place.idTown, - }; + const place: Place = await this.placeRepository.findOne({ + where: { idPlace }, + relations: ['categories'], + }); + + if (!place) { + throw new NotFoundException('Place not found'); + } + + // Update place properties + place.available = updatePlaceReqDto.available; + place.closeAt = updatePlaceReqDto.closeAt; + place.latitude = updatePlaceReqDto.latitude; + place.longitude = updatePlaceReqDto.longitude; + place.name = updatePlaceReqDto.name; + place.openAt = updatePlaceReqDto.openAt; + place.imageName = updatePlaceReqDto.image.filename; + + // Update categories + const categories: Category[] = []; + for (const categoryId of updatePlaceReqDto.categoriesId) { + const categoryEN: Category = await this.dataSource + .getRepository(Category) + .findOneByOrFail({ idCategory: categoryId }); + categoryEN.language = LANGUAGES.EN; + const categoryES: Category = { ...categoryEN, language: LANGUAGES.ES }; + categories.push(categoryEN); + categories.push(categoryES); + } + place.categories = categories; + + // Save the updated place + await this.placeRepository.save(place); - await this.placeRepository.update({ idPlace }, createPlaceDto); + // Update PlaceTraduction for EN await this.placeTraductionRepository .createQueryBuilder() .update(PlaceTraduction) @@ -175,6 +187,7 @@ export class PlaceService { .where('idPlace = :idPlace AND language = :language', { idPlace, language: LANGUAGES.EN }) .execute(); + // Update PlaceTraduction for ES await this.placeTraductionRepository .createQueryBuilder() .update(PlaceTraduction) @@ -182,6 +195,7 @@ export class PlaceService { .where('idPlace = :idPlace AND language = :language', { idPlace, language: LANGUAGES.ES }) .execute(); + // Update available date if necessary if (updatePlaceReqDto.available === Available.CUSTOM) { await this.availableDateRepository.update( { idPlace: place }, diff --git a/backend/src/town/entities/town.entity.ts b/backend/src/town/entities/town.entity.ts index 4b6a569f0186e796ce47315b82ad2fe637479c22..83fff7fa9c138a5af603a3c992e8533abe6f73dc 100644 --- a/backend/src/town/entities/town.entity.ts +++ b/backend/src/town/entities/town.entity.ts @@ -9,7 +9,7 @@ export class Town { @ManyToOne(() => State, (state) => state.stateId, { nullable: false, eager: true }) state: State; - @OneToMany(() => Place, (place) => place.idTown) + @OneToMany(() => Place, (place) => place.town) places: Place[]; @Column() diff --git a/backend/src/user/dto/update-preferedCategories.dto.ts b/backend/src/user/dto/update-preferedCategories.dto.ts new file mode 100644 index 0000000000000000000000000000000000000000..290f3ddc5cb1b8d48e1d662dad05646855f6ae7d --- /dev/null +++ b/backend/src/user/dto/update-preferedCategories.dto.ts @@ -0,0 +1,6 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class UpdatePreferedCategoriesDto { + @ApiProperty({ isArray: true, type: Number, example: [1, 2] }) + idCategories: number[]; +} diff --git a/backend/src/user/entities/user.entity.ts b/backend/src/user/entities/user.entity.ts index 01e24044695acaaa7bc9ec6e61bc3c2329e5edcd..43698b21e11247364dca95fd78c66f8fc553af0f 100644 --- a/backend/src/user/entities/user.entity.ts +++ b/backend/src/user/entities/user.entity.ts @@ -1,4 +1,5 @@ -import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm'; +import { Category } from 'src/category/entities/category.entity'; +import { Entity, Column, PrimaryGeneratedColumn, ManyToMany, JoinTable } from 'typeorm'; @Entity() export class User { @@ -10,6 +11,10 @@ export class User { }) email: string; + @JoinTable() + @ManyToMany(() => Category) + preferedCategories: Category[]; + @Column() name: string; diff --git a/backend/src/user/user.controller.ts b/backend/src/user/user.controller.ts index b95b2314be0e0a53d3b7b2c295a952477912803c..34e7c6d0fbbd6ff857e2db470241edb69107f13d 100644 --- a/backend/src/user/user.controller.ts +++ b/backend/src/user/user.controller.ts @@ -1,7 +1,43 @@ -import { Controller } from '@nestjs/common'; +import { Body, Controller, Get, Param, Patch, Req, UseGuards } from '@nestjs/common'; import { UserService } from './user.service'; +import { UpdatePreferedCategoriesDto } from './dto/update-preferedCategories.dto'; +import { CustomUserRequest } from 'src/auth/user/interface/customUserReq'; +import { ApiBearerAuth, ApiBody, ApiParam, ApiTags } from '@nestjs/swagger'; +import { AuthUserGuard } from 'src/auth/user/authUser.guard'; +import { LANGUAGES } from 'src/shared/enum/languages.enum'; @Controller('user') +@ApiTags('User') export class UserController { constructor(private readonly userService: UserService) {} + + @Patch('update-prefered-categories') + @ApiBearerAuth('jwt') + @ApiBody({ type: UpdatePreferedCategoriesDto }) + @UseGuards(AuthUserGuard) + async updatePreferedCategories( + @Body() updatePreferedCategoriesDto: UpdatePreferedCategoriesDto, + @Req() req: CustomUserRequest, + ) { + try { + const { email } = req.user; + await this.userService.updatePreferedCategories(email, updatePreferedCategoriesDto.idCategories); + return { message: 'Prefered categories updated successfully' }; + } catch (error) { + throw error; + } + } + + @Get('prefered-categories:lang') + @ApiBearerAuth('jwt') + @ApiParam({ name: 'lang', type: String }) + @UseGuards(AuthUserGuard) + async getPreferedCategories(@Req() req: CustomUserRequest, @Param('lang') lang: string) { + try { + const { email } = req.user; + return await this.userService.getPreferedCategories(email, lang as LANGUAGES); + } catch (error) { + throw error; + } + } } diff --git a/backend/src/user/user.module.ts b/backend/src/user/user.module.ts index 8a4003a16a1c30277f38833fab0bae0eb9e7313d..03b2f9d6eb422f95c62e0d281851373f15445e11 100644 --- a/backend/src/user/user.module.ts +++ b/backend/src/user/user.module.ts @@ -3,10 +3,15 @@ import { UserService } from './user.service'; import { UserController } from './user.controller'; import { TypeOrmModule } from '@nestjs/typeorm'; import { User } from './entities/user.entity'; +import { CategoryService } from 'src/category/category.service'; +import { Category } from 'src/category/entities/category.entity'; +import { AuthUserService } from 'src/auth/user/authUserservice'; +import { JwtService } from '@nestjs/jwt'; +import { EncryptionService } from 'src/auth/encryption/encryption.service'; @Module({ controllers: [UserController], - providers: [UserService], - imports: [TypeOrmModule.forFeature([User])], + providers: [UserService, CategoryService, AuthUserService, JwtService, EncryptionService], + imports: [TypeOrmModule.forFeature([User, Category])], }) export class UserModule {} diff --git a/backend/src/user/user.service.ts b/backend/src/user/user.service.ts index e22805e27f2fd4d69eb318b4ff751eb47ba2f1cc..63b2c3ccc5b92c70798ee9a05b7a1d8d9e77f869 100644 --- a/backend/src/user/user.service.ts +++ b/backend/src/user/user.service.ts @@ -1,15 +1,21 @@ -import { Injectable } from '@nestjs/common'; +import { BadRequestException, Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { User } from './entities/user.entity'; import { Repository } from 'typeorm'; import { CreateUserDto } from './dto/create-user.dto'; +import { Category } from 'src/category/entities/category.entity'; +import { CategoryService } from 'src/category/category.service'; +import { LANGUAGES } from 'src/shared/enum/languages.enum'; @Injectable() /** * Service responsible for handling user-related operations. */ export class UserService { - constructor(@InjectRepository(User) private userRepository: Repository) {} + constructor( + @InjectRepository(User) private userRepository: Repository, + private categoryService: CategoryService, + ) {} async findOne(email: string) { return await this.userRepository.findOneBy({ email }); @@ -28,4 +34,37 @@ export class UserService { async updatePassword(email: string, password: string) { await this.userRepository.update({ email }, { password }); } + + async updatePreferedCategories(email: string, idCategories: number[]) { + const user = await this.userRepository.findOneBy({ email }); + const categories: Category[] = []; + for (const idCategory of idCategories) { + const categoryEN: Category = await this.categoryService.findOne(idCategory); + if (!categoryEN) throw new BadRequestException('Category not found'); + categoryEN.language = LANGUAGES.EN; + const categoryES: Category = { + ...categoryEN, + language: LANGUAGES.ES, + }; + categories.push(categoryEN); + categories.push(categoryES); + } + user.preferedCategories = categories; + await this.userRepository.save(user); + } + + async getPreferedCategories(email: string, language: LANGUAGES): Promise { + const user: any = await this.userRepository.findOne({ + relations: { + preferedCategories: true, + }, + where: { + email, + preferedCategories: { + language, + }, + }, + }); + return user?.preferedCategories || []; + } }