diff --git a/.gitignore b/.gitignore index b82c94fbe7dec8073b73fe9914d08b263803dfaf..9d69baa07a9349a0ed0d73d664ad697aa5e65a9f 100644 --- a/.gitignore +++ b/.gitignore @@ -3,7 +3,8 @@ taller *.pdf **/target/ **/.vscode -*.ln -*.tmp **/tmp -*.log \ No newline at end of file +**/*.tmp +*.ln +*.log + diff --git a/angular/RestClient/src/app/app.routes.ts b/angular/RestClient/src/app/app.routes.ts index b8ee77b36e9b121a4cf804746bde023134978c1c..51aacbb7f9be2b3f9af5d3a7d4eaafb164c5ba26 100644 --- a/angular/RestClient/src/app/app.routes.ts +++ b/angular/RestClient/src/app/app.routes.ts @@ -5,6 +5,7 @@ import { HotelRegisterComponent } from './core/features/hotel/hotel-register/hot import { MainPageComponent } from './core/features/user/main-page/main-page.component'; import { BookingListComponent } from './core/features/bookings/booking-list/booking-list.component'; import { UserBookingListComponent } from './core/features/user/user-booking-list/user-booking-list.component'; +import { UserFormComponent } from './core/features/user/user-form/user-form.component'; export const routes: Routes = [ { @@ -35,6 +36,14 @@ export const routes: Routes = [ path: 'hotels/:id', component: HotelRegisterComponent, }, + { + path: 'users/:id', + component: UserFormComponent, + }, + { + path: 'register', + component: UserFormComponent, + }, { path: '**', redirectTo: '', diff --git a/angular/RestClient/src/app/core/features/user/main-page/main-page.component.ts b/angular/RestClient/src/app/core/features/user/main-page/main-page.component.ts index e609b7e568a909e2d6f7efd995ddccb57472cb76..c3506bbd37ebb81f35189e481d64e9875bac28b3 100644 --- a/angular/RestClient/src/app/core/features/user/main-page/main-page.component.ts +++ b/angular/RestClient/src/app/core/features/user/main-page/main-page.component.ts @@ -1,11 +1,11 @@ -// main-page.component.ts import { Component, OnInit } from '@angular/core'; -import { User, UserStateFilter } from '../../../../../types'; +import { Client, User, UserStateFilter } from '../../../../../types'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; -import users from '../../../../../mocks/users.json'; import { RouterModule } from '@angular/router'; import { UserClientService } from '../../../../shared/user-client.service'; +import { users } from '../../../../../mocks/users'; // Renombrado para claridad + @Component({ standalone: true, imports: [FormsModule, CommonModule, RouterModule], @@ -14,23 +14,32 @@ import { UserClientService } from '../../../../shared/user-client.service'; styleUrls: ['./main-page.component.css'], }) export class MainPageComponent implements OnInit { - users: User[] = []; - filteredUsers: User[] = []; + users: Client[] = []; + filteredUsers: Client[] = []; selectedStatus: UserStateFilter = 'All'; constructor(private userClient: UserClientService) {} ngOnInit(): void { - this.users = users as unknown as User[]; - this.userClient.getAllUsers().subscribe((data: User[]) => { - this.users = data; - this.filteredUsers = data; // Inicialmente, muestra todos los usuarios + // Validar que el mock sea del tipo correcto + // const isValidMock = Array.isArray(mockUsers) && mockUsers.every(user => 'id' in user && 'name' in user && 'status' in user); + // this.users = isValidMock ? (mockUsers as User[]) : []; + this.users = users; + this.filteredUsers = [...this.users]; + + // Sobrescribir con datos reales si están disponibles + this.userClient.getAllUsers().subscribe({ + next: (data: Client[]) => { + this.users = data; + this.filteredUsers = [...data]; + }, + error: (err) => console.error('Error al cargar usuarios:', err), }); } filterUsers(): void { if (this.selectedStatus === 'All') { - this.filteredUsers = this.users; + this.filteredUsers = [...this.users]; } else { this.filteredUsers = this.users.filter( (user) => user.status === this.selectedStatus @@ -38,7 +47,7 @@ export class MainPageComponent implements OnInit { } } - getState(user: User) { + getState(user: Client): string { switch (user.status) { case 'NO_BOOKINGS': return 'SIN RESERVAS'; @@ -46,6 +55,8 @@ export class MainPageComponent implements OnInit { return 'CON RESERVAS ACTIVAS'; case 'WITH_INACTIVE_BOOKINGS': return 'CON RESERVAS INACTIVAS'; + default: + return 'ESTADO DESCONOCIDO'; } } } diff --git a/angular/RestClient/src/app/core/features/user/user-form/user-form.component.css b/angular/RestClient/src/app/core/features/user/user-form/user-form.component.css new file mode 100644 index 0000000000000000000000000000000000000000..1eef9bba18e0bd279742ea1d297e8ab49da1c394 --- /dev/null +++ b/angular/RestClient/src/app/core/features/user/user-form/user-form.component.css @@ -0,0 +1,81 @@ +.form-container { + max-width: 400px; + margin: 50px auto; + background: #f8f9fa; + padding: 20px; + border-radius: 10px; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + font-family: Arial, sans-serif; +} + +h2 { + text-align: center; + margin-bottom: 20px; + color: #333; +} + +.form-group { + margin-bottom: 15px; +} + +label { + display: block; + font-weight: bold; + margin-bottom: 5px; + color: #555; +} + +input { + width: 100%; + padding: 10px; + font-size: 14px; + border: 1px solid #ccc; + border-radius: 5px; + background: #fff; +} + +input[readonly] { + background: #f3f3f3; +} + +.button-group { + display: flex; + justify-content: space-between; + margin-top: 20px; +} + +.btn { + padding: 10px 20px; + font-size: 14px; + border: none; + border-radius: 5px; + cursor: pointer; + transition: background-color 0.3s ease; +} + +.btn-primary { + background-color: #007bff; + color: white; +} + +.btn-primary:hover { + background-color: #0056b3; +} + +.btn-secondary { + background-color: #6c757d; + color: white; +} + +.btn-secondary:hover { + background-color: #5a6268; +} + +.btn-success { + background-color: #28a745; + color: white; +} + +.btn-success:hover { + background-color: #218838; +} diff --git a/angular/RestClient/src/app/core/features/user/user-form/user-form.component.html b/angular/RestClient/src/app/core/features/user/user-form/user-form.component.html new file mode 100644 index 0000000000000000000000000000000000000000..d68f32bfd2d6a14846503422e0273bc04effd127 --- /dev/null +++ b/angular/RestClient/src/app/core/features/user/user-form/user-form.component.html @@ -0,0 +1,75 @@ +<div class="form-container"> + <h2>Perfil de Usuario</h2> + <form [formGroup]="userForm" (ngSubmit)="saveChanges()"> + <!-- Campo Nombre --> + <div class="form-group"> + <label for="name">Nombre:</label> + <input + id="name" + type="text" + class="form-control" + formControlName="name" + [readonly]="!isEditing" + /> + </div> + + <!-- Campo Email --> + <div class="form-group"> + <label for="email">Email:</label> + <input + id="email" + type="email" + class="form-control" + formControlName="email" + [readonly]="!isEditing" + /> + </div> + + @if (isEditing) { + <!-- Campo Contraseña Actual (solo en edición) --> + <div class="form-group"> + <label for="currentPassword">Contraseña actual:</label> + <input + id="currentPassword" + type="password" + class="form-control" + formControlName="currentPassword" + placeholder="Introduce tu contraseña actual" + /> + </div> + + <!-- Campo Nueva Contraseña (solo en edición) --> + <div class="form-group"> + <label for="newPassword">Nueva contraseña:</label> + <input + id="newPassword" + type="password" + class="form-control" + formControlName="newPassword" + placeholder="Introduce tu nueva contraseña" + /> + </div> + } + + <!-- Grupo de Botones --> + <div class="button-group"> + @if (!isEditing) { + <button type="button" class="btn btn-primary" (click)="toggleEdit()"> + Editar + </button> + } @else { + <button type="button" class="btn btn-secondary" (click)="cancelEdit()"> + Cancelar + </button> + + <button + type="submit" + class="btn btn-success" + [disabled]="!userForm.valid" + > + Guardar + </button> + } + </div> + </form> +</div> diff --git a/angular/RestClient/src/app/core/features/user/user-form/user-form.component.spec.ts b/angular/RestClient/src/app/core/features/user/user-form/user-form.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..292f234075dbcba5482794cf23e4a231e666ba17 --- /dev/null +++ b/angular/RestClient/src/app/core/features/user/user-form/user-form.component.spec.ts @@ -0,0 +1,91 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { HttpResponse } from '@angular/common/http'; +import { UserFormComponent } from './user-form.component'; +import { UserClientService } from '../../../../shared/user-client.service'; +import { of } from 'rxjs'; + +describe('UserFormComponent', () => { + let component: UserFormComponent; + let fixture: ComponentFixture<UserFormComponent>; + let userService: UserClientService; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ReactiveFormsModule, FormsModule], + declarations: [UserFormComponent], + providers: [UserClientService], + }).compileComponents(); + + fixture = TestBed.createComponent(UserFormComponent); + component = fixture.componentInstance; + userService = TestBed.inject(UserClientService); + + spyOn(userService, 'getCurrentUser').and.returnValue( + of({ + id: 1, + name: 'John Doe', + email: 'johndoe@example.com', + rol: 'CONSUMER', + status: 'WITH_ACTIVE_BOOKINGS', + }) + ); + + spyOn(userService, 'updateUser').and.returnValue(of(new HttpResponse({ body: 'User updated successfully' }))); + spyOn(userService, 'updatePassword').and.returnValue(of(new HttpResponse({ body: 'Password updated successfully' }))); + + fixture.detectChanges(); + }); + + it('should create the component', () => { + expect(component).toBeTruthy(); + }); + + it('should load user data on initialization', () => { + expect(component.userForm.value).toEqual({ + name: 'John Doe', + email: 'johndoe@example.com', + password: '', + confirmPassword: '', + }); + }); + + it('should call updateUser when saving valid user data', () => { + component.userForm.patchValue({ + name: 'Jane Doe', + email: 'janedoe@example.com', + }); + + component.saveChanges(); + + expect(userService.updateUser).toHaveBeenCalledWith({ + name: 'Jane Doe', + email: 'janedoe@example.com', + }); + }); + + it('should call updatePassword when password is updated and matches confirmPassword', () => { + component.userForm.patchValue({ + password: 'newpassword123', + confirmPassword: 'newpassword123', + }); + + component.saveChanges(); + + expect(userService.updatePassword).toHaveBeenCalledWith( + '', + 'newpassword123' + ); + }); + + it('should not call updatePassword if password and confirmPassword do not match', () => { + component.userForm.patchValue({ + password: 'newpassword123', + confirmPassword: 'differentpassword', + }); + + component.saveChanges(); + + expect(userService.updatePassword).not.toHaveBeenCalled(); + }); +}); diff --git a/angular/RestClient/src/app/core/features/user/user-form/user-form.component.ts b/angular/RestClient/src/app/core/features/user/user-form/user-form.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..558e1c58a73e3d7ef1eaf852bce1a989a40bc42b --- /dev/null +++ b/angular/RestClient/src/app/core/features/user/user-form/user-form.component.ts @@ -0,0 +1,93 @@ +import { Component, OnInit } from '@angular/core'; +import { + FormBuilder, + FormGroup, + Validators, + ReactiveFormsModule, + FormsModule, +} from '@angular/forms'; +import { UserClientService } from '../../../../shared/user-client.service'; +import { users } from '../../../../../mocks/users'; +import { ActivatedRoute } from '@angular/router'; + +@Component({ + standalone: true, + selector: 'app-user-form', + templateUrl: './user-form.component.html', + styleUrls: ['./user-form.component.css'], + imports: [ReactiveFormsModule, FormsModule], +}) +export class UserFormComponent implements OnInit { + userForm!: FormGroup; + isEditing = false; + id = 0; + + constructor( + private fb: FormBuilder, + private userService: UserClientService, + private route: ActivatedRoute + ) {} + + ngOnInit(): void { + this.initializeForm(); + this.route.paramMap.subscribe({ + next: (params) => { + const id = Number(params.get('id')); + if (id) { + this.id = id; + this.isEditing = true; + this.loadUserData(); + } + }, + }); + } + + private initializeForm(): void { + this.userForm = this.fb.group({ + name: [{ value: '', disabled: true }, Validators.required], + email: [ + { value: '', disabled: true }, + [Validators.required, Validators.email], + ], + currentPassword: [''], // Solo habilitado en modo edición + newPassword: [''], // Solo habilitado en modo edición + }); + } + + private loadUserData(): void { + // this.userService.getCurrentUser().subscribe((user) => { + console.log({ id: this.id }); + users + .filter((u) => u.id == this.id) + .slice(0) + .map((user) => { + this.userForm.patchValue({ + name: user.name, + email: user.email, + }); + }); + } + + toggleEdit(): void { + this.isEditing = true; + this.userForm.get('name')?.enable(); + this.userForm.get('email')?.enable(); + } + + cancelEdit(): void { + this.isEditing = false; + this.loadUserData(); // Volver a cargar los datos originales + this.userForm.get('name')?.disable(); + this.userForm.get('email')?.disable(); + } + + saveChanges(): void { + if (this.userForm.valid) { + const updatedData = this.userForm.value; + this.userService.updateUser(updatedData).subscribe(() => { + this.isEditing = false; + this.loadUserData(); + }); + } + } +} diff --git a/angular/RestClient/src/app/core/navigation/navigation.component.html b/angular/RestClient/src/app/core/navigation/navigation.component.html index 63615464cbbf7aec69cbd99e5e33ea9d1e5e08d8..6ba3f139cd79d2fdfae6deb883b6a91e995ce6fd 100644 --- a/angular/RestClient/src/app/core/navigation/navigation.component.html +++ b/angular/RestClient/src/app/core/navigation/navigation.component.html @@ -8,5 +8,63 @@ <li> <a class="btn" [routerLink]="['/bookings', 'search']">Nueva Reserva</a> </li> + <li class="ml-auto"> + @if (isLogged){ + <!-- Dropdown para usuario registrado --> + <div class="relative ml-6"> + <button + (click)="toggleDropdown()" + class="flex items-center space-x-2 p-2 bg-blue-500 text-white rounded hover:bg-blue-600 focus:outline-none" + > + <span>{{ userName }}</span> + <svg + xmlns="http://www.w3.org/2000/svg" + class="h-5 w-5" + viewBox="0 0 20 20" + fill="currentColor" + > + <path + fill-rule="evenodd" + d="M5.23 7.21a.75.75 0 011.06.02L10 10.94l3.71-3.71a.75.75 0 111.06 1.06l-4 4a.75.75 0 01-1.06 0l-4-4a.75.75 0 01.02-1.06z" + clip-rule="evenodd" + /> + </svg> + </button> + + <!-- Menú desplegable --> + @if (dropdownOpen) { + <div + class="absolute right-0 mt-2 w-48 bg-slate-700 border border-gray-500 rounded shadow-lg" + > + <ul> + <li> + <a + class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100" + [routerLink]="['/bookings']" + > + Mis Reservas + </a> + </li> + <li> + <a + class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100" + > + <!-- (click)="logout()" --> + Cerrar Sesión + </a> + </li> + </ul> + </div> + } + </div> + + } @else { + <a + class="btn bg-blue-500 text-white ml-6 hover:bg-blue-600" + [routerLink]="['/login']" + >Login</a + > + } + </li> </ul> </nav> diff --git a/angular/RestClient/src/app/core/navigation/navigation.component.ts b/angular/RestClient/src/app/core/navigation/navigation.component.ts index ccdab77b49379e3ed27abd65ada67e693882939a..69efb6ad2307d3ac4f6865450372e280ec639519 100644 --- a/angular/RestClient/src/app/core/navigation/navigation.component.ts +++ b/angular/RestClient/src/app/core/navigation/navigation.component.ts @@ -1,5 +1,6 @@ import { Component } from '@angular/core'; import { Router, RouterModule } from '@angular/router'; +import { AuthClientService } from '../../shared/auth-client.service'; @Component({ selector: 'app-navigation', standalone: true, @@ -7,4 +8,24 @@ import { Router, RouterModule } from '@angular/router'; templateUrl: './navigation.component.html', styleUrl: './navigation.component.css', }) -export class NavigationComponent {} +export class NavigationComponent { + isLogged = true; + userName = 'User'; + dropdownOpen = false; + + constructor(private authClient: AuthClientService) {} + + toggleDropdown() { + this.dropdownOpen = !this.dropdownOpen; + this.authClient.login('migudel@dev.com', 'NQSASorry').subscribe({ + next: (response) => { + console.log(response); + alert('OKEY! You are logged'); + }, + error: (error) => { + console.error(error); + alert('Error! You are not logged'); + }, + }); + } +} diff --git a/angular/RestClient/src/app/shared/auth-client.service.ts b/angular/RestClient/src/app/shared/auth-client.service.ts index 61bf7a25a37f8e8208b30e789aa1086e72c6ba32..931fc7dbacbec3db5e5d048722fa5980017355e7 100644 --- a/angular/RestClient/src/app/shared/auth-client.service.ts +++ b/angular/RestClient/src/app/shared/auth-client.service.ts @@ -13,17 +13,17 @@ export class AuthClientService { login(email: String, password: String) { return this.http.post( `${this.URI}/login`, - { email, password }, - { - headers: { - 'Content-Type': 'application/json', - 'Access-Control-Allow-Origin': '*', - 'Access-Control-Allow-Methods': - 'GET, POST, OPTIONS, PUT, PATCH, DELETE', - 'Access-Control-Allow-Headers': 'X-Requested-With,content-type', - 'Access-Control-Allow-Credentials': 'true', - }, - } + { email, password } + // { + // headers: { + // 'Content-Type': 'application/json', + // 'Access-Control-Allow-Origin': '*', + // 'Access-Control-Allow-Methods': + // 'GET, POST, OPTIONS, PUT, PATCH, DELETE', + // 'Access-Control-Allow-Headers': 'X-Requested-With,content-type', + // 'Access-Control-Allow-Credentials': 'true', + // }, + // } ); } diff --git a/angular/RestClient/src/app/shared/user-client.service.ts b/angular/RestClient/src/app/shared/user-client.service.ts index 2a09c6d2c6b373ee3eb329d73066d84591f7fef3..7383a72b9aefd100bc87ebba38ef287e27eff3dd 100644 --- a/angular/RestClient/src/app/shared/user-client.service.ts +++ b/angular/RestClient/src/app/shared/user-client.service.ts @@ -1,25 +1,29 @@ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { environment } from '../../../environments/environment'; -import { User, UserState } from '../../types'; +import { Client, User, UserState } from '../../types'; @Injectable({ providedIn: 'root', }) export class UserClientService { private readonly URI = environment.userAPI; + constructor(private http: HttpClient) {} + // Obtener un usuario por ID getUser(userId: number) { return this.http.get<User>(`${this.URI}/${userId}`); } + // Obtener todos los usuarios getAllUsers() { - return this.http.get<User[]>(this.URI, { + return this.http.get<Client[]>(this.URI, { observe: 'body', }); } + // Cambiar estado de un usuario alterUserStatus(userId: number, status: UserState) { return this.http.patch( `${this.URI}/${userId}`, @@ -32,4 +36,29 @@ export class UserClientService { } ); } + + // Obtener el usuario actual (autenticado) + getCurrentUser() { + return this.http.get<User>(`${this.URI}/me`); + } + + // Actualizar los datos del usuario + updateUser(user: Partial<User>) { + return this.http.patch(`${this.URI}/me`, user, { + observe: 'response', + responseType: 'text', + }); + } + + // Cambiar la contraseña del usuario + updatePassword(currentPassword: string, newPassword: string) { + return this.http.patch( + `${this.URI}/me/password`, + { currentPassword, newPassword }, + { + observe: 'response', + responseType: 'text', + } + ); + } } diff --git a/angular/RestClient/src/mocks/users.ts b/angular/RestClient/src/mocks/users.ts new file mode 100644 index 0000000000000000000000000000000000000000..ffdf8f23af07980429bbb8cbf6a1e351bb555958 --- /dev/null +++ b/angular/RestClient/src/mocks/users.ts @@ -0,0 +1,18 @@ +import { Client, User } from '../types'; + +export const users: Client[] = [ + { + id: 1, + name: 'John Doe', + email: 'jon@com', + rol: 'CLIENT', + status: 'NO_BOOKINGS', + }, + { + id: 2, + name: 'Angela Doe', + email: 'angle@com', + rol: 'CLIENT', + status: 'NO_BOOKINGS', + }, +]; diff --git a/angular/RestClient/src/types/User.d.ts b/angular/RestClient/src/types/User.d.ts index 1ecc3d7512e7ff0b644e5555d290c48e88518219..277f806a74fb1b2f854d733cc10acca67f18b840 100644 --- a/angular/RestClient/src/types/User.d.ts +++ b/angular/RestClient/src/types/User.d.ts @@ -2,10 +2,20 @@ export interface User { id: number; name: string; email: String; - // status: "noBookings" | "withActiveBookings" | "withInactiveBookings"; + rol: UserRol; +} + +export interface Client extends User { status: UserState; + // bookings: number[] // Booking[] } +export interface HotelAdmin extends User { + // hotels: number[] // Hotel[] +} + +export type UserRol = 'ADMIN' | 'CLIENT' | 'HOTEL_ADMIN'; + export type UserStateFilter = 'All' | UserState; export type UserState =