Skip to content
Snippets Groups Projects
Commit 3bc78e90 authored by migudel's avatar migudel :speech_balloon:
Browse files

Edit-Register Hotel functional

parent 0fd8289c
No related branches found
No related tags found
2 merge requests!10Add ts types and json mocks, remove poblate scripts and fix the cascade...,!7Dev/angular/hotel register
Showing
with 1204 additions and 1223 deletions
......@@ -16,7 +16,7 @@
"outputPath": "dist/rest-client",
"index": "src/index.html",
"browser": "src/main.ts",
"polyfills": ["zone.js"],
"polyfills": ["zone.js", "@angular/localize/init"],
"tsConfig": "tsconfig.app.json",
"assets": [
{
......@@ -25,10 +25,14 @@
}
],
"styles": [
"node_modules/bootstrap/dist/css/bootstrap.min.css",
"@angular/material/prebuilt-themes/azure-blue.css",
"src/styles.css"
],
"scripts": [],
"scripts": [
"node_modules/jquery/dist/jquery.min.js",
"node_modules/bootstrap/dist/js/bootstrap.min.js"
],
"server": "src/main.server.ts",
"prerender": true,
"ssr": {
......@@ -77,7 +81,11 @@
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"polyfills": ["zone.js", "zone.js/testing"],
"polyfills": [
"zone.js",
"zone.js/testing",
"@angular/localize/init"
],
"tsConfig": "tsconfig.spec.json",
"assets": [
{
......@@ -86,7 +94,7 @@
}
],
"styles": [
"@angular/material/prebuilt-themes/cyan-orange.css",
"@angular/material/prebuilt-themes/azure-blue.css",
"src/styles.css"
],
"scripts": []
......
This diff is collapsed.
......@@ -23,7 +23,11 @@
"@angular/platform-server": "^18.2.0",
"@angular/router": "^18.2.0",
"@angular/ssr": "^18.2.7",
"@ng-bootstrap/ng-bootstrap": "^17.0.1",
"@popperjs/core": "^2.11.8",
"bootstrap": "^3.4.0",
"express": "^4.18.2",
"jquery": "^3.4.1",
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
"zone.js": "~0.14.10"
......@@ -32,6 +36,7 @@
"@angular-devkit/build-angular": "^18.2.7",
"@angular/cli": "^18.2.7",
"@angular/compiler-cli": "^18.2.0",
"@angular/localize": "^18.2.0",
"@types/express": "^4.17.17",
"@types/jasmine": "~5.1.0",
"@types/node": "^18.18.0",
......
<!-- compiled and minified CSS -->
<link
rel="stylesheet"
href="https://maxcdn.bootstrapcdn.com/bootstrap/3.4.0/css/bootstrap.min.css"
/>
<!-- jQuery library -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
<!-- compiled JavaScript -->
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.4.0/js/bootstrap.min.js"></script>
<div class="container">
<h3>Ejemplo angular. Gestión de reservas</h3>
<router-outlet></router-outlet>
......
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
import {
ApplicationConfig,
importProvidersFrom,
provideZoneChangeDetection,
} from '@angular/core';
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';
......@@ -11,6 +15,7 @@ export const appConfig: ApplicationConfig = {
provideZoneChangeDetection({ eventCoalescing: true }),
provideRouter(routes),
provideClientHydration(),
provideHttpClient(withFetch()), provideAnimationsAsync(),
provideHttpClient(withFetch()),
provideAnimationsAsync(),
],
};
import { Routes } from '@angular/router';
import { HotelListComponent } from './hotel-list/hotel-list.component';
import { HotelRegisterComponent } from './hotel-register/hotel-register.component';
export const routes: Routes = [
{
path: 'hotels',
component: HotelListComponent,
},
{
path: 'hotels/new',
component: HotelRegisterComponent,
},
{
path: 'hotels/:id',
component: HotelRegisterComponent,
},
{
path: '**',
redirectTo: 'hotels',
......
.header-text {
font-size: large;
}
.body-text {
font-size: small;
}
<div class="container">
<h2>Hotel List</h2>
@if (mostrarMensaje) {
<strong style="font-size: xx-large; color: red">{{ mensaje }}</strong>
}
<h2 style="text-align: center">Hotel List</h2>
<mat-accordion>
<mat-expansion-panel disabled="">
<mat-expansion-panel-header class="header-text">
<mat-panel-title>HOTEL</mat-panel-title>
<mat-panel-description>Location</mat-panel-description>
</mat-expansion-panel-header>
</mat-expansion-panel>
@for(hotel of hotels; track hotel.id) {
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title> {{ hotel.name }} </mat-panel-title>
<mat-expansion-panel expanded="true">
<mat-expansion-panel-header class="header-text">
<mat-panel-title> {{ hotel.id }}. {{ hotel.name }} </mat-panel-title>
<mat-panel-description>
{{ hotel.address.streetKind }} {{ hotel.address.streetName }} No.
{{ hotel.address.number }}, {{ hotel.address.postCode }}
......@@ -15,24 +18,56 @@
</mat-panel-description>
</mat-expansion-panel-header>
<div style="text-align: end; margin-bottom: 1rem">
<button
mat-raised-button
(click)="goToHotelDetails(hotel.id)"
style="
font-size: medium;
background-color: rgb(28, 197, 248);
color: rgb(250, 250, 250);
"
>
View hotel
</button>
<button
mat-raised-button
(click)="deleteHotel(hotel.id)"
style="
font-size: medium;
background-color: rgb(223, 36, 36);
color: rgb(250, 250, 250);
"
>
<strong> Delete Hotel </strong>
</button>
</div>
<table mat-table [dataSource]="hotel.rooms" class="mat-elevation-z8">
<ng-container matColumnDef="roomNumber">
<th mat-header-cell *matHeaderCellDef>Room Number</th>
<td mat-cell *matCellDef="let room">{{ room.roomNumber }}</td>
<th class="header-text" mat-header-cell *matHeaderCellDef>
Room Number
</th>
<td class="body-text" mat-cell *matCellDef="let room">
{{ room.roomNumber }}
</td>
</ng-container>
<ng-container matColumnDef="type">
<th mat-header-cell *matHeaderCellDef>Type</th>
<td mat-cell *matCellDef="let room">{{ room.type }}</td>
<th class="header-text" mat-header-cell *matHeaderCellDef>Type</th>
<td class="body-text" ce mat-cell *matCellDef="let room">
{{ room.type }}
</td>
</ng-container>
<ng-container matColumnDef="available">
<th mat-header-cell *matHeaderCellDef>Available</th>
<td mat-cell *matCellDef="let room">
<th class="header-text" mat-header-cell *matHeaderCellDef>
Available
</th>
<td class="body-text" mat-cell *matCellDef="let room">
<mat-slide-toggle
[checked]="room.available"
(change)="
toggleRoomAvailability(hotel.id, room.id, !room.available)
toggleRoomAvailability(hotel.id!, room.id, !room.available)
"
></mat-slide-toggle>
</td>
......@@ -47,10 +82,6 @@
*matRowDef="let row; columns: ['roomNumber', 'type', 'available']"
></tr>
</table>
<button mat-raised-button (click)="deleteHotel(hotel.id)">
Delete Hotel
</button>
</mat-expansion-panel>
}
</mat-accordion>
......
import { Component } from '@angular/core';
import { RouterModule, Router } from '@angular/router';
import { Hotel, Address } from '../../types';
import { Hotel } from '../../types';
import {
MatAccordion,
MatExpansionPanel,
......@@ -11,8 +11,7 @@ import {
import { MatSlideToggle } from '@angular/material/slide-toggle';
import { MatTable, MatTableModule } from '@angular/material/table';
import { MatButton } from '@angular/material/button';
import hotels from '../../mocks/hotels.json';
import { NgbAccordionModule } from '@ng-bootstrap/ng-bootstrap';
import { ClienteApiRestService } from '../shared/cliente-api-rest.service';
@Component({
......@@ -29,6 +28,7 @@ import { ClienteApiRestService } from '../shared/cliente-api-rest.service';
MatExpansionPanelHeader,
MatExpansionPanelTitle,
MatExpansionPanelDescription,
NgbAccordionModule,
],
templateUrl: './hotel-list.component.html',
styleUrl: './hotel-list.component.css',
......@@ -41,15 +41,14 @@ export class HotelListComponent {
constructor(private router: Router, private client: ClienteApiRestService) {}
ngOnInit() {
this.getHotelsResponse();
this.getHotels();
}
getHotelsResponse() {
// this.hotels = hotels as Hotel[];
// return;
getHotels() {
this.client.getAllHotels().subscribe({
next: (resp) => {
if (resp.body != null) this.hotels = [...resp.body];
console.warn({ resp });
if (!!resp || (resp as never[]).length != 0) this.hotels = [...resp];
},
error(err) {
console.log('Error al traer la lista: ' + err.message);
......@@ -68,7 +67,7 @@ export class HotelListComponent {
if (resp.status < 400) {
this.mostrarMensaje = true;
this.mensaje = resp.body as string;
this.getHotelsResponse();
this.getHotels();
} else {
this.mostrarMensaje = true;
this.mensaje = 'Error al eliminar registro';
......@@ -101,7 +100,7 @@ export class HotelListComponent {
if (resp.status < 400) {
this.mostrarMensaje = true;
this.mensaje = resp.body as string;
this.getHotelsResponse();
this.getHotels();
} else {
this.mostrarMensaje = true;
this.mensaje = 'Error al cambiar disponibilidad';
......@@ -115,7 +114,7 @@ export class HotelListComponent {
});
}
goToEdit(hotelId: number): void {
this.router.navigate(['/hotels', hotelId, 'edit']);
goToHotelDetails(hotelId: number): void {
this.router.navigate(['/hotels', hotelId]);
}
}
<div class="container">
<form [formGroup]="hotelForm">
<mat-card>
<mat-card-title class="fs-1 d-flex justify-content-center mt-5"
><strong>Registro de Hotel</strong></mat-card-title
>
<mat-card-content>
<div class="form-group">
<label for="name" class="fs-2">Nombre del Hotel</label>
<input
id="name"
class="form-control fs-3"
formControlName="name"
placeholder="Nombre del hotel"
/>
</div>
<div class="form-group mt-3">
<label class="fs-2">Dirección</label>
<div formGroupName="address">
<input
class="form-control mb-2 fs-3"
formControlName="streetKind"
placeholder="Tipo de calle"
/>
<input
class="form-control mb-2 fs-3"
formControlName="streetName"
placeholder="Nombre de la calle"
/>
<input
class="form-control mb-2 fs-3"
formControlName="number"
type="number"
min="1"
placeholder="Número"
/>
<input
class="form-control mb-2 fs-3"
formControlName="postCode"
placeholder="Código Postal"
/>
<input
class="form-control mb-2 fs-3"
formControlName="otherInfo"
placeholder="Otra información (opcional)"
/>
</div>
</div>
<!-- Lista de habitaciones -->
<div formArrayName="rooms">
<div class="d-flex gap-4 align-items-center mb-3">
<label class="fs-2">Habitaciones</label>
<button
class="btn btn-primary rounded-circle"
(click)="addRoom()"
[hidden]="editMode"
>
<strong class="fs-2">+</strong>
</button>
</div>
<div
*ngFor="let room of rooms.controls; let i = index"
[formGroupName]="i"
class="form-row row align-items-center mb-3"
>
<div
class="col-md-12 d-flex justify-content-between align-items-center mb-3"
>
<span class="d-flex gap-4 align-items-center pa fs-3">
<label>Habitación {{ i + 1 }}</label>
<!-- Disponibilidad de habitación -->
<mat-slide-toggle formControlName="available"
>Disponible</mat-slide-toggle
>
</span>
<button
class="btn btn-danger"
(click)="removeRoom(i)"
[disabled]="rooms.length <= 1"
[hidden]="editMode"
>
Eliminar
</button>
</div>
<!-- Número de habitación -->
<div class="col-md-6">
<mat-form-field appearance="fill" class="w-100">
<mat-label class="fs-3">Número de habitación</mat-label>
<input
matInput
formControlName="roomNumber"
placeholder="104A"
class="fs-3"
/>
</mat-form-field>
</div>
<!-- Tipo de habitación -->
<div class="col-md-6">
<mat-form-field appearance="fill" class="w-100">
<mat-label class="fs-3">Tipo de habitación</mat-label>
<mat-select formControlName="type" class="fs-3">
<mat-option class="fs-3" value="SINGLE">Single</mat-option>
<mat-option class="fs-3" value="DOUBLE">Double</mat-option>
<mat-option class="fs-3" value="SUITE">Suite</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
</div>
</mat-card-content>
<mat-card-actions
[hidden]="editMode"
class="d-flex justify-content-center"
>
<button
type="submit"
class="btn btn-success"
(click)="onSubmit()"
[disabled]="!hotelForm.valid"
>
<h4>Guardar Hotel</h4>
</button>
</mat-card-actions>
</mat-card>
</form>
</div>
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { HotelRegisterComponent } from './hotel-register.component';
describe('HotelRegisterComponent', () => {
let component: HotelRegisterComponent;
let fixture: ComponentFixture<HotelRegisterComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [HotelRegisterComponent]
})
.compileComponents();
fixture = TestBed.createComponent(HotelRegisterComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
import { Component } from '@angular/core';
import {
AbstractControl,
FormArray,
FormBuilder,
FormGroup,
ReactiveFormsModule,
Validators,
} from '@angular/forms';
import { MatCardModule } from '@angular/material/card';
import { MatInputModule } from '@angular/material/input';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatSelectModule } from '@angular/material/select';
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
import { CommonModule } from '@angular/common';
import { ClienteApiRestService } from '../shared/cliente-api-rest.service';
import { Address, Hotel, Room } from '../../types';
import { ActivatedRoute, Router } from '@angular/router';
const emptyHotel: Hotel = {
id: 0,
name: '',
rooms: [
{
id: 0,
available: false,
roomNumber: '',
type: 'SINGLE',
},
],
address: {
id: 0,
number: 0,
streetKind: '',
postCode: '',
streetName: '',
},
};
@Component({
selector: 'app-hotel-register',
standalone: true,
imports: [
CommonModule,
ReactiveFormsModule,
MatCardModule,
MatInputModule,
MatFormFieldModule,
MatSelectModule,
MatSlideToggleModule,
],
templateUrl: './hotel-register.component.html',
styleUrl: './hotel-register.component.css',
})
export class HotelRegisterComponent {
editMode: boolean;
hotelForm: FormGroup;
constructor(
private router: Router,
private route: ActivatedRoute,
private fb: FormBuilder,
private client: ClienteApiRestService
) {
this.hotelForm = this.setHotelForm();
this.editMode = false;
this.route.paramMap.subscribe({
next: (params) => {
const id = Number(params.get('id'));
this.editMode = id !== 0;
if (this.editMode) {
this.client.getHotel(id).subscribe({
next: (h) => this.setHotelForm(h),
error: (error) => {
router.navigate(['/hotels/new']);
},
});
}
console.warn({ id });
},
});
}
get rooms(): FormArray {
return this.hotelForm.get('rooms') as FormArray;
}
// Agregar una nueva habitación
addRoom(): void {
const roomForm = this.fb.group({
roomNumber: ['', Validators.required],
type: ['SINGLE', Validators.required],
available: [true],
});
this.rooms.push(roomForm);
}
// Eliminar habitación
removeRoom(index: number): void {
alert('remove action');
this.rooms.removeAt(index);
}
// Método de envío del formulario
onSubmit(): void {
if (this.hotelForm.valid) {
const hotel = this.hotelForm.value as Hotel;
console.log(hotel);
this.client.addHotel(hotel).subscribe({
next: (resp) => {
if (resp.status < 400) {
alert('Hotel guardado correctamente');
} else {
}
},
error: (err) => {
console.log('Error al borrar: ' + err.message);
throw err;
},
});
}
}
setHotelForm({
name,
address,
rooms,
}: {
name: String;
address: Address;
rooms: Room[];
} = emptyHotel) {
const form = this.fb.group({
name: [name, Validators.required],
address: this.fb.group({
streetKind: [address.streetKind, Validators.required],
streetName: [address.streetName, Validators.required],
number: [address.number, [Validators.required, Validators.min(1)]],
postCode: [address.postCode, Validators.required],
otherInfo: address.otherInfo,
}),
rooms: this.fb.array(
rooms.map(
(room) =>
this.fb.group({
roomNumber: [room.roomNumber, Validators.required],
type: [room.type, Validators.required],
available: [true],
}),
Validators.required
)
),
});
if (this.editMode) form.disable();
this.hotelForm = form;
return form;
}
}
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import hotels from '../../mocks/hotels.json';
import { Hotel } from '../../types';
@Injectable({
......@@ -12,9 +10,14 @@ export class ClienteApiRestService {
private static readonly HOTEL_URI = `${ClienteApiRestService.BASE_URI}/hotels`;
constructor(private http: HttpClient) {}
getHotel(id: number) {
const url = `${ClienteApiRestService.HOTEL_URI}/${id}`;
return this.http.get<Hotel>(url);
}
getAllHotels() {
const url = `${ClienteApiRestService.HOTEL_URI}`;
return this.http.get<Hotel[]>(url, { observe: 'response' });
return this.http.get<Hotel[]>(url);
}
deleteHotel(id: number) {
......
/// <reference types="@angular/localize" />
import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig } from './app/app.config';
import { AppComponent } from './app/app.component';
bootstrapApplication(AppComponent, appConfig)
.catch((err) => console.error(err));
bootstrapApplication(AppComponent, appConfig).catch((err) =>
console.error(err)
);
......@@ -5,7 +5,8 @@
"compilerOptions": {
"outDir": "./out-tsc/app",
"types": [
"node"
"node",
"@angular/localize"
]
},
"files": [
......
......@@ -5,7 +5,8 @@
"compilerOptions": {
"outDir": "./out-tsc/spec",
"types": [
"jasmine"
"jasmine",
"@angular/localize"
]
},
"include": [
......
......@@ -34,7 +34,7 @@ public class Room {
@JsonIgnore
private Hotel hotel;
@Column(name = "room_number", nullable = false)
private int roomNumber;
private String roomNumber;
@Column(name = "type", nullable = false)
private Tipo type;
@Column(name = "available", nullable = false)
......@@ -46,7 +46,7 @@ public class Room {
public Room() {
}
public Room(int id, Hotel hotelId, int roomNumber, Tipo type, boolean available, List<Booking> bookings) {
public Room(int id, Hotel hotelId, String roomNumber, Tipo type, boolean available, List<Booking> bookings) {
this.id = id;
this.hotel = hotelId;
this.roomNumber = roomNumber;
......@@ -71,11 +71,11 @@ public class Room {
return this.hotel;
}
public void setRoomNumber(int roomNumber) {
public void setRoomNumber(String roomNumber) {
this.roomNumber = roomNumber;
}
public int getRoomNumber() {
public String getRoomNumber() {
return this.roomNumber;
}
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please to comment