diff --git a/customer/app/package-lock.json b/customer/app/package-lock.json index 14c714b..4cc2cb9 100644 --- a/customer/app/package-lock.json +++ b/customer/app/package-lock.json @@ -498,6 +498,35 @@ } } }, + "@fortawesome/angular-fontawesome": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@fortawesome/angular-fontawesome/-/angular-fontawesome-0.4.0.tgz", + "integrity": "sha512-DYVXdCzwQo6d0CxVMRK+10LpBAvYN9xigWeQW4wKYq/Czd5es46nPMKixB5rHfNViECwwlM2gTM61K4DpxlJxg==", + "requires": { + "tslib": "^1.9.0" + } + }, + "@fortawesome/fontawesome-common-types": { + "version": "0.2.19", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.2.19.tgz", + "integrity": "sha512-nd2Ul/CUs8U9sjofQYAALzOGpgkVJQgEhIJnOHaoyVR/LeC3x2mVg4eB910a4kS6WgLPebAY0M2fApEI497raQ==" + }, + "@fortawesome/fontawesome-svg-core": { + "version": "1.2.19", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-1.2.19.tgz", + "integrity": "sha512-D4ICXg9oU08eF9o7Or392gPpjmwwgJu8ecCFusthbID95CLVXOgIyd4mOKD9Nud5Ckz+Ty59pqkNtThDKR0erA==", + "requires": { + "@fortawesome/fontawesome-common-types": "^0.2.19" + } + }, + "@fortawesome/free-solid-svg-icons": { + "version": "5.9.0", + "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-5.9.0.tgz", + "integrity": "sha512-U8YXPfWcSozsCW0psCtlRGKjjRs5+Am5JJwLOUmVHFZbIEWzaz4YbP84EoPwUsVmSAKrisu3QeNcVOtmGml0Xw==", + "requires": { + "@fortawesome/fontawesome-common-types": "^0.2.19" + } + }, "@ngtools/webpack": { "version": "8.0.6", "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-8.0.6.tgz", diff --git a/customer/app/package.json b/customer/app/package.json index f1e42f9..f830b27 100644 --- a/customer/app/package.json +++ b/customer/app/package.json @@ -19,6 +19,9 @@ "@angular/platform-browser": "~8.0.3", "@angular/platform-browser-dynamic": "~8.0.3", "@angular/router": "~8.0.3", + "@fortawesome/angular-fontawesome": "^0.4.0", + "@fortawesome/fontawesome-svg-core": "^1.2.19", + "@fortawesome/free-solid-svg-icons": "^5.9.0", "bootstrap": "^4.3.1", "ngx-bootstrap": "^5.0.0", "rxjs": "~6.4.0", diff --git a/customer/app/src/app/_guards/auth.guard.ts b/customer/app/src/app/_guards/auth.guard.ts index 598e472..039dcee 100644 --- a/customer/app/src/app/_guards/auth.guard.ts +++ b/customer/app/src/app/_guards/auth.guard.ts @@ -1,5 +1,6 @@ import { Injectable } from "@angular/core"; import { CanActivate, Router, ActivatedRouteSnapshot, RouterStateSnapshot } from "@angular/router"; +import {AuthenticationService} from "../services/authentication.service"; /* The auth guard is used to prevent unauthenticated users from accessing restricted routes. @@ -13,10 +14,10 @@ There can be other conditions too, like role based authentication @Injectable({providedIn: 'root'}) export class AuthGuard implements CanActivate{ - constructor(private router: Router){} + constructor(private router: Router, private authenticationService: AuthenticationService){} canActivate(router: ActivatedRouteSnapshot, state: RouterStateSnapshot){ // check if the user is logged in - if(localStorage.getItem('currentUser')){ + if(this.authenticationService.isLoggedIn()){ return true; } diff --git a/customer/app/src/app/_helpers/fake-backend.ts b/customer/app/src/app/_helpers/fake-backend.ts index b02b267..6213d55 100644 --- a/customer/app/src/app/_helpers/fake-backend.ts +++ b/customer/app/src/app/_helpers/fake-backend.ts @@ -1,7 +1,17 @@ -import { Injectable } from "@angular/core"; -import { HttpRequest, HttpResponse, HttpHandler, HttpEvent, HttpInterceptor, HTTP_INTERCEPTORS } from '@angular/common/http'; -import { Observable, of, throwError } from 'rxjs'; -import { delay, mergeMap, materialize, dematerialize } from 'rxjs/operators';; +import {Injectable} from "@angular/core"; +import { + HttpRequest, + HttpResponse, + HttpHandler, + HttpEvent, + HttpInterceptor, + HTTP_INTERCEPTORS +} from '@angular/common/http'; +import {Observable, of, throwError} from 'rxjs'; +import {delay, mergeMap, materialize, dematerialize} from 'rxjs/operators'; +import {Event, EventType, Trainer} from "../services/event.service"; + +; /* FAKE BACKEND @@ -19,92 +29,180 @@ API @Injectable() -export class FakeBackendInterceptor implements HttpInterceptor{ - - constructor(){} +export class FakeBackendInterceptor implements HttpInterceptor { - intercept(request: HttpRequest, next: HttpHandler): Observable>{ - // test user - (one of the users detais in database) - let testUser = { - id: 1, - username: 'test', - password: 'test', - firstName: 'Test', - lastName: 'User' + constructor() { + } + + intercept(request: HttpRequest, next: HttpHandler): Observable> { + // test user - (one of the users detais in database) + let testUser = { + id: 1, + username: 'test', + password: 'test', + firstName: 'Test', + lastName: 'User' + }; + + let eventTypes: EventType[] = [ + { + id: 1, + name: 'Típus 1' + }, + { + id: 2, + name: 'Típus 2' + } + ]; + + let trainer1: Trainer = { + id: 1, + name: "Fitness Trainer" + }; + + let trainer2: Trainer = { + id: 1, + name: "weight lift Trainer" + }; + + let events: Event[] = [ + ]; + + let id = 1000; + for (let dayIndex = 0; dayIndex < 28; dayIndex++) { + for (let hourIndex = 8; hourIndex < 22; hourIndex++) { + for (let itemIndex = 0; itemIndex < 3; itemIndex++) { + + id = id + 1; + let reservedAt = id % 2 ? this.createEventDate(0,1) : null; + events.push( + { + id: id, + name: 'esemény ' + id, + start: this.createEventDate(dayIndex, hourIndex), + end: this.createEventDate(dayIndex, hourIndex + 1), + trainer: [trainer1, trainer2][id % 2], + eventType: eventTypes[id % eventTypes.length], + reservedAt: reservedAt, + reservationCount: id% 11, + seatCount: 10, + } + ) + } + } + } + console.info('events.size', events.length); + + + // wrapping the API's in delayed observable to simulate the Server API Calls + return of(null).pipe( + mergeMap(() => { + + // API 1: Authenticate - Will be hit by Authentication Service - STARTS + if (request.url.endsWith('/users/authenticate') && request.method === 'POST') { + + + //check the credentials entered by the user with the data in database + if (request.body.username === testUser.username && request.body.password === testUser.password) { + + // if login details are valid, return status 200 with a Fake JWT Token + let body = { + id: testUser.id, + username: testUser.username, + firstName: testUser.firstName, + lastName: testUser.lastName, + token: '0000-fake-jwt-token-0000' + } + + return of(new HttpResponse({status: 200, body})) + } else { + // if the credentials by user doesn't match the data in the db, return Status 400 - Bad Request + return throwError({ + error: { + message: 'Username or Password is incorrect.' + } + }) + } + } + // API 1: Authenticate - Will be hit by Authentication Service - ENDS + + + // SECURE API END POINT - will check for valid JWT Token in Request + // API 2: Get all users data (we now have only 1 user - testUser) - STARTS + if (request.url.endsWith('/users') && request.method === 'GET') { + + // check for a fake jwt token. If valid JWT token found, return the list of users, else throw error + if (request.headers.get('Authorization') === 'Bearer 0000-fake-jwt-token-0000') { + return of(new HttpResponse({status: 200, body: [testUser]})); + } else { + // invalid JWT token found in request header + return throwError({ + error: { + message: 'Unauthorized' + } + }); + } + } + // API 2: Get all users data (we now have only 1 user - testUser) - ENDS + + + if (request.url.endsWith('/event-type') && request.method === 'GET') { + + // check for a fake jwt token. If valid JWT token found, return the list of users, else throw error + if (request.headers.get('Authorization') === 'Bearer 0000-fake-jwt-token-0000') { + return of(new HttpResponse({status: 200, body: eventTypes})); + } else { + // invalid JWT token found in request header + return throwError({ + error: { + message: 'Unauthorized' + } + }); + } } - // wrapping the API's in delayed observable to simulate the Server API Calls - return of(null).pipe( - mergeMap(() => { - // API 1: Authenticate - Will be hit by Authentication Service - STARTS - if(request.url.endsWith('/users/authenticate') && request.method === 'POST'){ + if (request.url.indexOf('/events&id_event_type') && request.method === 'GET') { + + // check for a fake jwt token. If valid JWT token found, return the list of users, else throw error + if (request.headers.get('Authorization') === 'Bearer 0000-fake-jwt-token-0000') { + return of(new HttpResponse({status: 200, body: events})); + } else { + // invalid JWT token found in request header + return throwError({ + error: { + message: 'Unauthorized' + } + }); + } + } - //check the credentials entered by the user with the data in database - if(request.body.username === testUser.username && request.body.password === testUser.password){ - - // if login details are valid, return status 200 with a Fake JWT Token - let body = { - id: testUser.id, - username: testUser.username, - firstName: testUser.firstName, - lastName: testUser.lastName, - token: '0000-fake-jwt-token-0000' - } + // Pass any other requests left (unhandled + return next.handle(request); + }) + ) + // call materialize and dematerialize to ensure delay even if an error is thrown + .pipe(materialize()) + .pipe(delay(500)) + .pipe(dematerialize()); + } - return of(new HttpResponse({status: 200, body})) - } - else { - // if the credentials by user doesn't match the data in the db, return Status 400 - Bad Request - return throwError({ - error: { - message: 'Username or Password is incorrect.' - } - }) - } - } - // API 1: Authenticate - Will be hit by Authentication Service - ENDS - - - - // SECURE API END POINT - will check for valid JWT Token in Request - // API 2: Get all users data (we now have only 1 user - testUser) - STARTS - if(request.url.endsWith('/users') && request.method === 'GET'){ - - // check for a fake jwt token. If valid JWT token found, return the list of users, else throw error - if(request.headers.get('Authorization') === 'Bearer 0000-fake-jwt-token-0000'){ - return of(new HttpResponse({status: 200, body: [testUser]})); - } - else{ - // invalid JWT token found in request header - return throwError({ - error: { - message: 'Unauthorized' - } - }); - } - } - // API 2: Get all users data (we now have only 1 user - testUser) - ENDS - - - // Pass any other requests left (unhandled - return next.handle(request); - }) - ) - // call materialize and dematerialize to ensure delay even if an error is thrown - .pipe(materialize()) - .pipe(delay(500)) - .pipe(dematerialize()); - } + createEventDate(plusDays: number, plusHours: number) { + let date = new Date(); + date = new Date(date.getTime() + (plusDays * 1000 * 60 * 60 * 24)); + date.setHours(plusHours); + return date.getTime(); + } } + // creating a PROVIDER export let fakeBackendProvider = { - // use fake backend in place of Http service for backend-less development - provide: HTTP_INTERCEPTORS, - useClass: FakeBackendInterceptor, - multi: true + // use fake backend in place of Http service for backend-less development + provide: HTTP_INTERCEPTORS, + useClass: FakeBackendInterceptor, + multi: true }; diff --git a/customer/app/src/app/app.module.ts b/customer/app/src/app/app.module.ts index cb388f8..8390480 100644 --- a/customer/app/src/app/app.module.ts +++ b/customer/app/src/app/app.module.ts @@ -1,12 +1,12 @@ import { BrowserModule } from '@angular/platform-browser'; -import { NgModule } from '@angular/core'; +import {LOCALE_ID, NgModule} from '@angular/core'; import { AppRoutingModule } from './app-routing.module'; import { AppComponent } from './app.component'; import { FitNavigationComponent } from './components/fit-navigation/fit-navigation.component'; import { FitSlidesComponent } from './components/fit-slides/fit-slides.component'; import {BrowserAnimationsModule} from "@angular/platform-browser/animations"; -import {CollapseModule} from "ngx-bootstrap"; +import {CollapseModule, TypeaheadModule} from "ngx-bootstrap"; import { HomeComponent } from './pages/home/home.component'; import { LoginComponent } from './pages/login/login.component'; import { ProfileComponent } from './pages/profile/profile.component'; @@ -15,6 +15,19 @@ import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http'; import { fakeBackendProvider } from './_helpers'; import {ReactiveFormsModule} from "@angular/forms"; import { EventsComponent } from './pages/events/events.component'; +import { FitEventTypesComponent } from './components/fit-event-types/fit-event-types.component'; + +import { registerLocaleData } from '@angular/common'; +import localeHu from '@angular/common/locales/hu'; +import {FontAwesomeModule} from "@fortawesome/angular-fontawesome"; +import { library } from '@fortawesome/fontawesome-svg-core'; + +import { faUserPlus , faUserMinus } from '@fortawesome/free-solid-svg-icons'; + + + +// the second parameter 'fr' is optional +registerLocaleData(localeHu, 'hu'); @NgModule({ @@ -26,7 +39,7 @@ import { EventsComponent } from './pages/events/events.component'; LoginComponent, ProfileComponent, EventsComponent, - + FitEventTypesComponent, ], imports: [ BrowserModule, @@ -34,9 +47,12 @@ import { EventsComponent } from './pages/events/events.component'; ReactiveFormsModule, BrowserAnimationsModule, CollapseModule.forRoot(), - HttpClientModule + HttpClientModule, + TypeaheadModule.forRoot(), + FontAwesomeModule, ], providers: [ + { provide: LOCALE_ID, useValue: "hu-hu" }, { provide: HTTP_INTERCEPTORS, useClass: JwtInterceptor, multi: true }, { provide: HTTP_INTERCEPTORS, useClass: ErrorInterceptor, multi: true }, @@ -45,4 +61,10 @@ import { EventsComponent } from './pages/events/events.component'; ], bootstrap: [AppComponent] }) -export class AppModule { } +export class AppModule { + constructor(){ + library.add(faUserPlus); + library.add(faUserMinus); + } + +} diff --git a/customer/app/src/app/pages/events/events.component.html b/customer/app/src/app/pages/events/events.component.html index f2f77fd..e4088a4 100644 --- a/customer/app/src/app/pages/events/events.component.html +++ b/customer/app/src/app/pages/events/events.component.html @@ -1 +1,48 @@ -

events works!

+
+
+
+ +
+
+ +
+

{{eventDay.date | date }}

+
+ +
+
+

{{eventHour.hour | date:'HH:mm'}}

+
+
+
+ +
+
+
+ + {{event.start | date:'HH:mm' }} - {{event.end | date:'HH:mm' }} + +
+
+ + {{event.eventType.name }} ( {{event.reservationCount}}/{{event.seatCount}} + +
+
+ + {{event.trainer.name}} + +
+
+ Jelentkezem + Leiratkozom + Nincs szabad hely +
+
+
+
+
+
+
+ +
diff --git a/customer/app/src/app/pages/events/events.component.scss b/customer/app/src/app/pages/events/events.component.scss index e69de29..52ae6e2 100644 --- a/customer/app/src/app/pages/events/events.component.scss +++ b/customer/app/src/app/pages/events/events.component.scss @@ -0,0 +1,22 @@ +.event-item{padding: 2px; + + .btn-primary { + color: gray; + &:hover{ + background-color: black; + color: #e5ce48; + border: 1px solid #e5ce48; + } + } +} + + +.event-item:nth-child(even) {background: lightgrey;} +.event-item:nth-child(odd) { + background: gray; + color: #ffffff; + +} +.event-item:hover { background: orange; cursor: pointer; } + + diff --git a/customer/app/src/app/pages/events/events.component.ts b/customer/app/src/app/pages/events/events.component.ts index 9c877eb..a3b4abd 100644 --- a/customer/app/src/app/pages/events/events.component.ts +++ b/customer/app/src/app/pages/events/events.component.ts @@ -1,4 +1,9 @@ -import { Component, OnInit } from '@angular/core'; +import {Component, OnInit} from '@angular/core'; +import {Event, EventService, EventType} from "../../services/event.service"; +import {FormBuilder, FormGroup, Validators} from "@angular/forms"; +import {of, pipe} from "rxjs"; +import {flatMap} from "rxjs/operators"; +import {findAll} from "@angular/compiler-cli/ngcc/src/utils"; @Component({ selector: 'app-events', @@ -7,9 +12,148 @@ import { Component, OnInit } from '@angular/core'; }) export class EventsComponent implements OnInit { - constructor() { } + types: EventType[]; + eventTypeForm: FormGroup; + + originalEvents: Event[] = []; + events: Event[] = []; + + eventDays: EventDate[] = []; + + selectedEventType: number = null; + + + constructor(private eventService: EventService, + private formBuilder: FormBuilder,) { + + this.eventTypeForm = this.formBuilder.group({ + eventType: ['', [Validators.required]], + }); + + this.eventTypeForm.get('eventType').valueChanges.subscribe((params) => { + console.info('event type value change', params); + this.selectedEventType = params; + this.filterEvents(); + }); + + } + + filterEvents() { + console.info(this.events); + this.eventTypeForm.get('eventType'); + this.events = this.originalEvents; + if (this.selectedEventType && this.selectedEventType > 0) { + this.events = this.originalEvents.filter( + value => value.eventType.id == this.selectedEventType + ); + } + this.prepareEventDates(this.events); + } + ngOnInit() { + of(null).pipe( + flatMap(() => this.eventService.findAllEventTypes()), + flatMap(value => { + this.types = value; + return of(null) + }), + flatMap(() => this.eventService.findEvents()), + flatMap(value => { + this.originalEvents = value; + return of(null) + }), + flatMap(() => { + this.filterEvents(); + return of(null) + }) + ).subscribe(); + } + + mayRegister(event: Event) { + return event.reservedAt == null && event.reservationCount < event.seatCount; + } + + mayCancel(event: Event) { + return event.reservedAt; + } + + noFreeSeat(event: Event) { + return event.reservedAt == null && event.reservationCount >= event.seatCount; + } + + prepareEventDates(events: Event[]) { + this.eventDays = []; + let date = new Date(); + date.setSeconds(0); + date.setMilliseconds(0); + + // we will display events for the next 28 days + for (let dayIndex = 0; dayIndex < 14; dayIndex++) { + // the day with the current time + let day = new Date(date.getTime() + (dayIndex * 1000 * 60 * 60 * 24)); + let dayAt = day; + // the day with start of day + let dayStartAt = new Date(dayAt.getTime()); + dayStartAt.setHours(0, 0, 0, 0); + let eventDate = { + date: day.getTime(), + hours: [] + }; + let hourIndex = 0; + let iterate = true; + + // on the current date we will display events from this timestamp. + // We need this, because we display on the first day from the current timestampe + // but on the other days, from the start of the day + let startDisplayAt = dayIndex == 0 ? dayAt.getTime() : dayStartAt.getTime(); + // for each day add all hours in the present and future + while (iterate) { + + let hour = new Date(startDisplayAt + (hourIndex * (1000 * 60 * 60))); + hour.setHours(hour.getHours(), 0, 0, 0); + // we have increased the hour of the day + // if there was no dayswitch because of the hour increase, we can add this hour slot to the current hour + iterate = hour.getMonth() == dayAt.getMonth() && hour.getDate() == dayAt.getDate(); + hourIndex = hourIndex + 1; + // add the hour + if (iterate) { + let eventHour = { + hour: hour.getTime(), + events: [] + }; + // iterate over all events, and put each event to it's hour slot + for (let j = 0; j < events.length; j++) { + let currentEvent = events[j]; + let hourStart = hour; + let hourEnd = new Date(hour.getTime() + (1000 * 60 * 60)); + // if event is in this day + if (currentEvent.start >= hourStart.getTime() && currentEvent.start < hourEnd.getTime()) { + eventHour.events.push(currentEvent); + } + } + if (eventHour.events.length) { + eventDate.hours.push(eventHour); + } + } + } + if (eventDate.hours.length) { + this.eventDays.push(eventDate); + } + + } } } + + +export interface EventDate { + date: number; + hours: EventHour[]; +} + +export interface EventHour { + hour: number; + events: Event[]; + +} diff --git a/customer/app/src/app/services/authentication.service.ts b/customer/app/src/app/services/authentication.service.ts index 2f9aadc..e1cfc68 100644 --- a/customer/app/src/app/services/authentication.service.ts +++ b/customer/app/src/app/services/authentication.service.ts @@ -9,7 +9,12 @@ import {BehaviorSubject} from "rxjs"; }) export class AuthenticationService { private _user: BehaviorSubject = new BehaviorSubject(null); - constructor(private http: HttpClient){} + constructor(private http: HttpClient){ + let user = localStorage.getItem('currentUser' ); + if ( user ){ + this.user.next( JSON.stringify(user)); + } + } // login login(username: string, password:string){ @@ -34,6 +39,10 @@ export class AuthenticationService { return this._user; } + public isLoggedIn(){ + return this.user.value; + } + // logout logout(){ // remove user from local storage diff --git a/customer/app/src/app/services/endpoints.ts b/customer/app/src/app/services/endpoints.ts index 0cede1a..37aacdb 100644 --- a/customer/app/src/app/services/endpoints.ts +++ b/customer/app/src/app/services/endpoints.ts @@ -1,3 +1,6 @@ +import {Observable} from "rxjs"; +import {EventType} from "./event.service"; + export class Endpoints { private static contextPath = "/api"; private static baseUrl: string = Endpoints.contextPath + "/rest"; @@ -6,4 +9,12 @@ export class Endpoints { return `${this.baseUrl}/users/authenticate`; } + public static GET_EVENTS(eventType: number){ + return `${this.baseUrl}/events&id_event_type=${eventType}`; + } + + public static GET_EVENT_TYPES(){ + return `${this.baseUrl}/event-type`; + } + } diff --git a/customer/app/src/app/services/models.ts b/customer/app/src/app/services/models.ts new file mode 100644 index 0000000..43474c4 --- /dev/null +++ b/customer/app/src/app/services/models.ts @@ -0,0 +1,4 @@ + +export interface Reservation { + idEvent: number; +}