From 48e36f2541e41510334eee221d54c7277a12cdcb Mon Sep 17 00:00:00 2001 From: Bastyen Date: Tue, 9 Jul 2024 15:34:47 +0200 Subject: [PATCH 01/12] add api services --- front-end/src/app/app.component.ts | 12 ++- front-end/src/app/app.config.ts | 13 ++- front-end/src/app/app.routes.ts | 1 - .../src/app/interceptors/http.interceptor.ts | 90 +++++++++++++++++++ .../create-account.component.html | 14 +++ .../create-account.component.ts | 27 +++++- .../forget-password.component.html | 2 +- .../src/app/pages/login/login.component.ts | 25 +++++- .../my-account/my-account.component.html | 8 ++ .../pages/my-account/my-account.component.ts | 30 +++++-- .../synthesis-interface.component.ts | 26 ++++-- front-end/src/app/services/auth.service.ts | 86 ++++++++++++++++-- .../app/services/observations.service.spec.ts | 16 ++++ .../src/app/services/observations.service.ts | 30 +++++++ .../src/app/services/settings.service.spec.ts | 16 ++++ .../src/app/services/settings.service.ts | 23 +++++ 16 files changed, 386 insertions(+), 33 deletions(-) create mode 100644 front-end/src/app/interceptors/http.interceptor.ts create mode 100644 front-end/src/app/services/observations.service.spec.ts create mode 100644 front-end/src/app/services/observations.service.ts create mode 100644 front-end/src/app/services/settings.service.spec.ts create mode 100644 front-end/src/app/services/settings.service.ts diff --git a/front-end/src/app/app.component.ts b/front-end/src/app/app.component.ts index 3aa8ccc..ab8d2eb 100644 --- a/front-end/src/app/app.component.ts +++ b/front-end/src/app/app.component.ts @@ -18,11 +18,12 @@ import { import { MatListModule } from '@angular/material/list'; import { MatSidenav, MatSidenavModule } from '@angular/material/sidenav'; import { Location } from '@angular/common'; -import { AuthService } from './services/auth.service'; +import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar'; import { Platform } from '@angular/cdk/platform'; import { MatBadgeModule } from '@angular/material/badge'; import { MatDividerModule } from '@angular/material/divider'; import { OfflineService } from './services/offline.service'; +import { AuthService } from './services/auth.service'; @Component({ selector: 'app-root', @@ -38,6 +39,7 @@ import { OfflineService } from './services/offline.service'; MatListModule, MatBadgeModule, MatDividerModule, + MatSnackBarModule, ], templateUrl: './app.component.html', styleUrl: './app.component.scss', @@ -52,6 +54,7 @@ export class AppComponent { location = inject(Location); authService = inject(AuthService); offlineService = inject(OfflineService); + snackBar = inject(MatSnackBar); @ViewChild('sidenav') private sidenav!: MatSidenav; @@ -112,6 +115,8 @@ export class AppComponent { click: () => { this.authService.logout(); this.sidenav.close(); + this.snackBar.open('Vous êtes déconnecté', '', { duration: 2000 }); + this.router.navigate(['..']); }, observationsPending: false, }, @@ -144,6 +149,11 @@ export class AppComponent { ngOnInit() { this.authService.isAuth.subscribe((value) => { + if (value) { + this.authService.getAccount().subscribe((account) => { + console.log(account); + }); + } this.handleAuthentification(value); }); this.router.events.subscribe((event) => { diff --git a/front-end/src/app/app.config.ts b/front-end/src/app/app.config.ts index 82042cc..72b9526 100644 --- a/front-end/src/app/app.config.ts +++ b/front-end/src/app/app.config.ts @@ -10,9 +10,17 @@ import { provideClientHydration } from '@angular/platform-browser'; import { provideAnimationsAsync } from '@angular/platform-browser/animations/async'; import { MAT_DATE_LOCALE } from '@angular/material/core'; import { provideMomentDateAdapter } from '@angular/material-moment-adapter'; -import { provideHttpClient, withFetch } from '@angular/common/http'; +import { + provideHttpClient, + withFetch, + withInterceptorsFromDi, +} from '@angular/common/http'; import { provideServiceWorker } from '@angular/service-worker'; +import { httpInterceptorProviders } from './interceptors/http.interceptor'; +export function tokenGetter() { + return localStorage.getItem('access_token'); +} export const appConfig: ApplicationConfig = { providers: [ provideZoneChangeDetection({ eventCoalescing: true }), @@ -31,7 +39,8 @@ export const appConfig: ApplicationConfig = { monthYearA11yLabel: 'MMMM YYYY', }, }), - provideHttpClient(withFetch()), + httpInterceptorProviders, + provideHttpClient(withInterceptorsFromDi(), withFetch()), provideServiceWorker('ngsw-worker.js', { enabled: !isDevMode(), registrationStrategy: 'registerWhenStable:30000', diff --git a/front-end/src/app/app.routes.ts b/front-end/src/app/app.routes.ts index 1ffee9f..2ad93e5 100644 --- a/front-end/src/app/app.routes.ts +++ b/front-end/src/app/app.routes.ts @@ -91,7 +91,6 @@ export const routes: Routes = [ backButton: true, accountButton: false, }, - canActivate: [authGuard], }, { path: 'mon-compte', diff --git a/front-end/src/app/interceptors/http.interceptor.ts b/front-end/src/app/interceptors/http.interceptor.ts new file mode 100644 index 0000000..622dbfe --- /dev/null +++ b/front-end/src/app/interceptors/http.interceptor.ts @@ -0,0 +1,90 @@ +import { inject, Injectable } from '@angular/core'; +import { + HttpEvent, + HttpInterceptor, + HttpHandler, + HttpRequest, + HTTP_INTERCEPTORS, + HttpErrorResponse, +} from '@angular/common/http'; +import { + BehaviorSubject, + catchError, + filter, + Observable, + switchMap, + take, + throwError, +} from 'rxjs'; +import { AuthService } from '../services/auth.service'; + +const TOKEN_HEADER_KEY = 'Authorization'; +@Injectable() +export class HttpRequestInterceptor implements HttpInterceptor { + isRefreshing = false; + refreshTokenSubject: BehaviorSubject = new BehaviorSubject(null); + + authService = inject(AuthService); + + intercept( + req: HttpRequest, + next: HttpHandler, + ): Observable> { + if (req.url.includes('account') && !req.url.includes('sign-up')) { + const token = this.authService.getToken(); + if (token) { + req = this.addTokenHeader(req, token); + } + } + + return next.handle(req).pipe( + catchError((error) => { + if (error instanceof HttpErrorResponse && error.status === 401) { + return this.handle401Error(req, next); + } + return throwError(() => new Error('Erreur')); + }), + ); + } + + handle401Error(req: HttpRequest, next: HttpHandler) { + if (!this.isRefreshing) { + this.isRefreshing = true; + this.refreshTokenSubject.next(null); + + const refreshToken = this.authService.getRefreshToken(); + + if (refreshToken) { + return this.authService.refreshToken(refreshToken).pipe( + switchMap((token: any) => { + this.isRefreshing = false; + this.authService.saveToken(token.accessToken); + + return next.handle(this.addTokenHeader(req, token.accessToken)); + }), + catchError(() => { + this.isRefreshing = false; + this.authService.logout(); + return throwError(() => new Error('Erreur')); + }), + ); + } + } + + return this.refreshTokenSubject.pipe( + filter((token) => token !== null), + take(1), + switchMap((token) => next.handle(this.addTokenHeader(req, token))), + ); + } + + addTokenHeader(req: HttpRequest, token: string) { + return req.clone({ + headers: req.headers.set(TOKEN_HEADER_KEY, `Bearer ${token}`), + }); + } +} + +export const httpInterceptorProviders = [ + { provide: HTTP_INTERCEPTORS, useClass: HttpRequestInterceptor, multi: true }, +]; diff --git a/front-end/src/app/pages/create-account/create-account.component.html b/front-end/src/app/pages/create-account/create-account.component.html index 5630978..eb5340c 100644 --- a/front-end/src/app/pages/create-account/create-account.component.html +++ b/front-end/src/app/pages/create-account/create-account.component.html @@ -12,6 +12,20 @@ Une adresse email est nécessaire } + + Nom + + @if (lastNameFormControl.hasError("required")) { + Un nom nécessaire + } + + + Prénom + + @if (emailFormControl.hasError("required")) { + Un prénom nécessaire + } + Mot de passe diff --git a/front-end/src/app/pages/create-account/create-account.component.ts b/front-end/src/app/pages/create-account/create-account.component.ts index bed6af6..94a2d2f 100644 --- a/front-end/src/app/pages/create-account/create-account.component.ts +++ b/front-end/src/app/pages/create-account/create-account.component.ts @@ -10,6 +10,8 @@ import { MatButtonModule } from '@angular/material/button'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; import { Router, RouterLink } from '@angular/router'; +import { AuthService } from '../../services/auth.service'; +import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar'; @Component({ selector: 'app-create-account', @@ -22,6 +24,7 @@ import { Router, RouterLink } from '@angular/router'; ReactiveFormsModule, MatButtonModule, RouterLink, + MatSnackBarModule, ], templateUrl: './create-account.component.html', styleUrl: './create-account.component.scss', @@ -31,6 +34,8 @@ export class CreateAccountComponent { Validators.required, Validators.email, ]); + firstNameFormControl = new FormControl('', [Validators.required]); + lastNameFormControl = new FormControl('', [Validators.required]); passwordFormControl = new FormControl('', [ Validators.required, Validators.minLength(6), @@ -43,6 +48,8 @@ export class CreateAccountComponent { passwordError = false; router = inject(Router); + authService = inject(AuthService); + snackBar = inject(MatSnackBar); ngOnInit() { this.passwordFormControl.valueChanges.subscribe((value) => { @@ -67,10 +74,28 @@ export class CreateAccountComponent { this.passwordError = false; if ( this.emailFormControl.valid && + this.firstNameFormControl.valid && + this.lastNameFormControl.valid && this.passwordFormControl.valid && this.confirmPasswordFormControl.valid ) { - this.router.navigate(['/se-connecter']); + this.authService + .createAccount({ + email: this.emailFormControl.value!, + last_name: this.lastNameFormControl.value!, + first_name: this.firstNameFormControl.value!, + password: this.passwordFormControl.value!, + }) + .subscribe({ + next: (success) => { + console.log('success', success); + this.snackBar.open('Compte créé', '', { duration: 2000 }); + this.router.navigate(['/se-connecter']); + }, + error: (error) => { + console.log('error', error); + }, + }); } } else { this.passwordError = true; diff --git a/front-end/src/app/pages/forget-password/forget-password.component.html b/front-end/src/app/pages/forget-password/forget-password.component.html index 57b38ca..37d1549 100644 --- a/front-end/src/app/pages/forget-password/forget-password.component.html +++ b/front-end/src/app/pages/forget-password/forget-password.component.html @@ -12,7 +12,7 @@ Une adresse email est nécessaire } - diff --git a/front-end/src/app/pages/login/login.component.ts b/front-end/src/app/pages/login/login.component.ts index e5ca340..598653e 100644 --- a/front-end/src/app/pages/login/login.component.ts +++ b/front-end/src/app/pages/login/login.component.ts @@ -10,6 +10,7 @@ import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; import { Router, RouterLink } from '@angular/router'; import { AuthService } from '../../services/auth.service'; +import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar'; @Component({ selector: 'app-login', @@ -21,6 +22,7 @@ import { AuthService } from '../../services/auth.service'; ReactiveFormsModule, MatButtonModule, RouterLink, + MatSnackBarModule, ], templateUrl: './login.component.html', styleUrl: './login.component.scss', @@ -37,13 +39,28 @@ export class LoginComponent { router = inject(Router); authService = inject(AuthService); + snackBar = inject(MatSnackBar); onLogin() { if (this.emailFormControl.valid && this.passwordFormControl.valid) { - this.authService.login(); - if (this.authService.isAuth.value) { - this.router.navigate(['..']); - } + this.authService + .login({ + email: this.emailFormControl.value!, + password: this.passwordFormControl.value!, + }) + .subscribe({ + next: (success: any) => { + console.log('success', success); + this.snackBar.open('Vous êtes connecté', '', { duration: 2000 }); + this.authService.saveToken(success.access); + this.authService.saveRefreshToken(success.refresh); + this.authService.checkAuth(); + this.router.navigate(['..']); + }, + error: (error) => { + console.log('error', error); + }, + }); } } } diff --git a/front-end/src/app/pages/my-account/my-account.component.html b/front-end/src/app/pages/my-account/my-account.component.html index eda628d..0f0a780 100644 --- a/front-end/src/app/pages/my-account/my-account.component.html +++ b/front-end/src/app/pages/my-account/my-account.component.html @@ -3,6 +3,14 @@ Email + + Nom + + + + Prénom + +
diff --git a/front-end/src/app/pages/my-account/my-account.component.ts b/front-end/src/app/pages/my-account/my-account.component.ts index 66d078f..257cbc5 100644 --- a/front-end/src/app/pages/my-account/my-account.component.ts +++ b/front-end/src/app/pages/my-account/my-account.component.ts @@ -1,10 +1,5 @@ import { Component, inject } from '@angular/core'; -import { - FormControl, - FormGroup, - FormsModule, - ReactiveFormsModule, -} from '@angular/forms'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { MatButtonModule } from '@angular/material/button'; import { MatDialog } from '@angular/material/dialog'; import { MatFormFieldModule } from '@angular/material/form-field'; @@ -12,6 +7,7 @@ import { MatInputModule } from '@angular/material/input'; import { Router, RouterLink } from '@angular/router'; import { DeleteAccountDialog } from './dialogs/delete-account-dialog'; import { AuthService } from '../../services/auth.service'; +import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar'; @Component({ selector: 'app-my-account', @@ -23,6 +19,7 @@ import { AuthService } from '../../services/auth.service'; ReactiveFormsModule, MatButtonModule, RouterLink, + MatSnackBarModule, ], templateUrl: './my-account.component.html', styleUrl: './my-account.component.scss', @@ -30,16 +27,33 @@ import { AuthService } from '../../services/auth.service'; export class MyAccountComponent { readonly dialog = inject(MatDialog); email = 'adresse@email.com'; + lastName = 'Nom'; + firstName = 'Prénom'; + authService = inject(AuthService); router = inject(Router); + snackBar = inject(MatSnackBar); openDeleteAccountDialog() { const deleteDialogRef = this.dialog.open(DeleteAccountDialog); deleteDialogRef.afterClosed().subscribe((result) => { if (result && result.delete) { - this.authService.logout(); - this.router.navigate(['..']); + this.authService.deleteAccount().subscribe({ + next: (success: any) => { + console.log('success', success); + this.authService.removeToken(); + this.authService.removeRefreshToken(); + this.authService.checkAuth(); + this.snackBar.open('Votre compte est supprimé', '', { + duration: 2000, + }); + this.router.navigate(['..']); + }, + error: (error) => { + console.log('error', error); + }, + }); } }); } diff --git a/front-end/src/app/pages/synthesis-interface/synthesis-interface.component.ts b/front-end/src/app/pages/synthesis-interface/synthesis-interface.component.ts index e57c9e1..cbfbb54 100644 --- a/front-end/src/app/pages/synthesis-interface/synthesis-interface.component.ts +++ b/front-end/src/app/pages/synthesis-interface/synthesis-interface.component.ts @@ -23,6 +23,7 @@ import { Observations } from '../../types/types'; import evenementsRemarquables from '../../../data/evenements_remarquables.json'; import observationTypes from '../../../data/types.json'; +import { ObservationsService } from '../../services/observations.service'; @Component({ selector: 'app-synthesis-interface', @@ -57,6 +58,7 @@ export class SynthesisInterfaceComponent { observationsLayer: L.GeoJSON | null = null; observationsClusterGroup: any; router = inject(Router); + observationsService = inject(ObservationsService); expansionPanelIsOpen = false; bounds: any; @@ -74,21 +76,27 @@ export class SynthesisInterfaceComponent { } async initMap() { + this.observationsService.getObservations().subscribe({ + next: (success) => { + console.log('success', success); + }, + error: (error) => { + console.log('error', error); + }, + }); this.L = await import('leaflet'); await import('leaflet.locatecontrol'); await import('leaflet.markercluster'); - const tileLayerOffline = await import('leaflet.offline'); + const { tileLayerOffline } = await import('leaflet.offline'); this.map = this.L.default.map('map', { zoom: 4, center: [47, 2] }); - this.L.default - .tileLayerOffline( - 'https://data.geopf.fr/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=GEOGRAPHICALGRIDSYSTEMS.PLANIGNV2&STYLE=normal&FORMAT=image/png&TILEMATRIXSET=PM&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}', - { - attribution: "IGN", - }, - ) - .addTo(this.map); + tileLayerOffline( + 'https://data.geopf.fr/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=GEOGRAPHICALGRIDSYSTEMS.PLANIGNV2&STYLE=normal&FORMAT=image/png&TILEMATRIXSET=PM&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}', + { + attribution: "IGN", + }, + ).addTo(this.map); this.L.default.control .locate({ setView: 'once', showPopup: false }) diff --git a/front-end/src/app/services/auth.service.ts b/front-end/src/app/services/auth.service.ts index c743835..2842387 100644 --- a/front-end/src/app/services/auth.service.ts +++ b/front-end/src/app/services/auth.service.ts @@ -1,7 +1,12 @@ import { Injectable } from '@angular/core'; -import { HttpClient } from '@angular/common/http'; +import { HttpClient, HttpHeaders } from '@angular/common/http'; import { inject } from '@angular/core'; import { BehaviorSubject } from 'rxjs'; +import { environment } from '../../environments/environment'; + +const httpOptions = { + headers: new HttpHeaders({ 'Content-Type': 'application/json' }), +}; @Injectable({ providedIn: 'root', @@ -11,16 +16,85 @@ export class AuthService { isAuth = new BehaviorSubject(false); checkAuth() { - this.isAuth.next(localStorage.getItem('token') !== null); + this.isAuth.next(localStorage.getItem('access_token') !== null); } - login() { - localStorage.setItem('token', 'token'); - this.checkAuth(); + getAccount() { + return this.httpClient.get( + `${environment.apiUrl}/api/accounts/me/`, + httpOptions, + ); + } + + login(account: { email: string; password: string }) { + return this.httpClient.post( + `${environment.apiUrl}/api/token/`, + account, + httpOptions, + ); } logout() { - localStorage.removeItem('token'); + this.removeToken(); + this.removeRefreshToken(); this.checkAuth(); } + + createAccount(account: { + email: string; + last_name: string; + first_name: string; + password: string; + }) { + return this.httpClient.post( + `${environment.apiUrl}/api/accounts/sign-up/`, + account, + httpOptions, + ); + } + + deleteAccount() { + return this.httpClient.delete( + `${environment.apiUrl}/api/accounts/me/`, + httpOptions, + ); + } + + changePassword() {} + + resetPassword() {} + + refreshToken(token: string) { + return this.httpClient.post( + `${environment.apiUrl}/api/token/refresh/`, + { + refreshToken: token, + }, + httpOptions, + ); + } + + getToken() { + return localStorage.getItem('access_token'); + } + + getRefreshToken() { + return localStorage.getItem('refresh_token'); + } + + saveToken(token: string) { + localStorage.setItem('access_token', token); + } + + saveRefreshToken(token: string) { + localStorage.setItem('refresh_token', token); + } + + removeToken() { + localStorage.removeItem('access_token'); + } + + removeRefreshToken() { + localStorage.removeItem('refresh_token'); + } } diff --git a/front-end/src/app/services/observations.service.spec.ts b/front-end/src/app/services/observations.service.spec.ts new file mode 100644 index 0000000..517bc98 --- /dev/null +++ b/front-end/src/app/services/observations.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { ObservationsService } from './observations.service'; + +describe('ObservationsService', () => { + let service: ObservationsService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(ObservationsService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/front-end/src/app/services/observations.service.ts b/front-end/src/app/services/observations.service.ts new file mode 100644 index 0000000..48ddecf --- /dev/null +++ b/front-end/src/app/services/observations.service.ts @@ -0,0 +1,30 @@ +import { HttpClient, HttpHeaders } from '@angular/common/http'; +import { inject, Injectable } from '@angular/core'; +import { environment } from '../../environments/environment'; + +const httpOptions = { + headers: new HttpHeaders({ 'Content-Type': 'application/json' }), +}; + +@Injectable({ + providedIn: 'root', +}) +export class ObservationsService { + httpClient = inject(HttpClient); + + constructor() {} + + getObservations() { + return this.httpClient.get( + `${environment.apiUrl}/api/observations/`, + httpOptions, + ); + } + + getObservation(observationId: number) { + return this.httpClient.get( + `${environment.apiUrl}/api/observations/${observationId}/`, + httpOptions, + ); + } +} diff --git a/front-end/src/app/services/settings.service.spec.ts b/front-end/src/app/services/settings.service.spec.ts new file mode 100644 index 0000000..359cb6b --- /dev/null +++ b/front-end/src/app/services/settings.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { SettingsService } from './settings.service'; + +describe('SettingsService', () => { + let service: SettingsService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(SettingsService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/front-end/src/app/services/settings.service.ts b/front-end/src/app/services/settings.service.ts new file mode 100644 index 0000000..b6816e2 --- /dev/null +++ b/front-end/src/app/services/settings.service.ts @@ -0,0 +1,23 @@ +import { HttpClient, HttpHeaders } from '@angular/common/http'; +import { inject, Injectable } from '@angular/core'; +import { environment } from '../../environments/environment'; + +const httpOptions = { + headers: new HttpHeaders({ 'Content-Type': 'application/json' }), +}; + +@Injectable({ + providedIn: 'root', +}) +export class SettingsService { + httpClient = inject(HttpClient); + + constructor() {} + + getSettings() { + return this.httpClient.get( + `${environment.apiUrl}/api/settings/`, + httpOptions, + ); + } +} From fb8d55d0bb3af15d1fcbdee076ed9229dc2b6f89 Mon Sep 17 00:00:00 2001 From: Bastyen Date: Wed, 10 Jul 2024 17:39:22 +0200 Subject: [PATCH 02/12] get data from api --- front-end/src/app/app.component.html | 8 +- front-end/src/app/app.component.scss | 20 ++- front-end/src/app/app.component.ts | 20 ++- front-end/src/app/app.routes.ts | 6 +- .../src/app/interceptors/http.interceptor.ts | 5 +- .../change-password.component.ts | 20 ++- .../my-account/my-account.component.html | 21 ++- .../pages/my-account/my-account.component.ts | 2 + .../my-observations.component.html | 23 +-- .../my-observations.component.ts | 31 +++- .../my-offline-data.component.html | 49 +++--- .../my-offline-data.component.scss | 5 +- .../my-offline-data.component.ts | 11 +- .../new-observation.component.html | 13 +- .../new-observation.component.scss | 5 +- .../new-observation.component.ts | 15 +- .../observation-detail.component.html | 66 ++++---- .../observation-detail.component.ts | 35 ++-- .../dialogs/filter-dialog.html | 4 +- .../dialogs/filter-dialog.ts | 10 +- .../synthesis-interface.component.html | 21 +-- .../synthesis-interface.component.scss | 4 + .../synthesis-interface.component.ts | 149 ++++++++++-------- front-end/src/app/services/auth.service.ts | 22 ++- .../src/app/services/observations.service.ts | 14 +- front-end/src/app/services/offline.service.ts | 4 + .../src/app/services/settings.service.ts | 8 + front-end/src/app/types/types.ts | 15 +- front-end/src/data/areas.json | 56 ------- .../src/data/evenements_remarquables.json | 51 ------ front-end/src/data/types.json | 94 ----------- 31 files changed, 395 insertions(+), 412 deletions(-) delete mode 100644 front-end/src/data/areas.json delete mode 100644 front-end/src/data/evenements_remarquables.json delete mode 100644 front-end/src/data/types.json diff --git a/front-end/src/app/app.component.html b/front-end/src/app/app.component.html index a0d952e..c580338 100644 --- a/front-end/src/app/app.component.html +++ b/front-end/src/app/app.component.html @@ -7,7 +7,11 @@ @@ -42,7 +46,7 @@
diff --git a/front-end/src/app/app.component.scss b/front-end/src/app/app.component.scss index 69f5198..4bc4efc 100644 --- a/front-end/src/app/app.component.scss +++ b/front-end/src/app/app.component.scss @@ -1,14 +1,26 @@ -main { - height: calc(100vh - var(--mat-toolbar-standard-height)); +mat-sidenav-container { + height: calc( + 100vh - var(--mat-toolbar-standard-height) - var( + --mat-toolbar-standard-height + ) + ); background: var(--mat-toolbar-container-background-color); } @media (max-width: 599px) { - main { - height: calc(100vh - var(--mat-toolbar-mobile-height)); + mat-sidenav-container { + height: calc( + 100vh - var(--mat-toolbar-mobile-height) - var( + --mat-toolbar-mobile-height + ) + ); } } +main { + height: 100%; +} + .toolbar-spacer { flex: 1 1 auto; } diff --git a/front-end/src/app/app.component.ts b/front-end/src/app/app.component.ts index ab8d2eb..f6fd104 100644 --- a/front-end/src/app/app.component.ts +++ b/front-end/src/app/app.component.ts @@ -24,6 +24,7 @@ import { MatBadgeModule } from '@angular/material/badge'; import { MatDividerModule } from '@angular/material/divider'; import { OfflineService } from './services/offline.service'; import { AuthService } from './services/auth.service'; +import { SettingsService } from './services/settings.service'; @Component({ selector: 'app-root', @@ -53,6 +54,8 @@ export class AppComponent { router = inject(Router); location = inject(Location); authService = inject(AuthService); + settingsService = inject(SettingsService); + offlineService = inject(OfflineService); snackBar = inject(MatSnackBar); @@ -101,8 +104,8 @@ export class AppComponent { }, { id: 6, - text: 'Mes données hors ligne', - routerLink: 'mes-donnees-hors-ligne', + text: 'Fonds de carte hors ligne', + routerLink: 'fonds-de-carte-hors-ligne', authenficated: true, click: () => null, observationsPending: false, @@ -143,19 +146,26 @@ export class AppComponent { this.authService.checkAuth(); } afterNextRender(() => { - this.offlineService.handleObservationsPending(); + if (this.authService.isAuth) { + this.offlineService.handleObservationsPending(); + } }); } ngOnInit() { this.authService.isAuth.subscribe((value) => { if (value) { - this.authService.getAccount().subscribe((account) => { - console.log(account); + this.authService.getAccount().subscribe((account: any) => { + this.authService.setUser(account); }); } this.handleAuthentification(value); }); + + this.settingsService.getSettings().subscribe((settings: any) => { + this.settingsService.setSettings(settings); + }); + this.router.events.subscribe((event) => { if (event instanceof ActivationEnd) { if (this.sidenav.opened) { diff --git a/front-end/src/app/app.routes.ts b/front-end/src/app/app.routes.ts index 2ad93e5..8fb97b2 100644 --- a/front-end/src/app/app.routes.ts +++ b/front-end/src/app/app.routes.ts @@ -107,11 +107,11 @@ export const routes: Routes = [ canActivate: [authGuard], }, { - path: 'mes-donnees-hors-ligne', - title: 'Mes données hors ligne', + path: 'fonds-de-carte-hors-ligne', + title: 'Fonds de carte hors ligne', component: MyOfflineDataComponent, data: { - title: 'Mes données hors ligne', + title: 'Fonds de carte hors ligne', backButton: true, accountButton: false, }, diff --git a/front-end/src/app/interceptors/http.interceptor.ts b/front-end/src/app/interceptors/http.interceptor.ts index 622dbfe..8a94243 100644 --- a/front-end/src/app/interceptors/http.interceptor.ts +++ b/front-end/src/app/interceptors/http.interceptor.ts @@ -58,9 +58,8 @@ export class HttpRequestInterceptor implements HttpInterceptor { return this.authService.refreshToken(refreshToken).pipe( switchMap((token: any) => { this.isRefreshing = false; - this.authService.saveToken(token.accessToken); - - return next.handle(this.addTokenHeader(req, token.accessToken)); + this.authService.saveToken(token.access); + return next.handle(this.addTokenHeader(req, token.access)); }), catchError(() => { this.isRefreshing = false; diff --git a/front-end/src/app/pages/change-password/change-password.component.ts b/front-end/src/app/pages/change-password/change-password.component.ts index 92dd8e4..b82e6a1 100644 --- a/front-end/src/app/pages/change-password/change-password.component.ts +++ b/front-end/src/app/pages/change-password/change-password.component.ts @@ -11,6 +11,7 @@ import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; import { Router, RouterLink } from '@angular/router'; import { AuthService } from '../../services/auth.service'; +import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar'; @Component({ selector: 'app-change-password', @@ -23,6 +24,7 @@ import { AuthService } from '../../services/auth.service'; ReactiveFormsModule, MatButtonModule, RouterLink, + MatSnackBarModule, ], templateUrl: './change-password.component.html', styleUrl: './change-password.component.scss', @@ -41,6 +43,7 @@ export class ChangePasswordComponent { router = inject(Router); authService = inject(AuthService); + snackBar = inject(MatSnackBar); ngOnInit() { this.passwordFormControl.valueChanges.subscribe((value) => { @@ -67,8 +70,21 @@ export class ChangePasswordComponent { this.passwordFormControl.valid && this.confirmPasswordFormControl.valid ) { - this.authService.logout(); - this.router.navigate(['/se-connecter']); + this.authService + .changePassword(this.passwordFormControl.value!) + .subscribe({ + next: (success) => { + console.log('success', success); + this.authService.logout(); + this.snackBar.open('Vous êtes déconnecté', '', { + duration: 2000, + }); + this.router.navigate(['/se-connecter']); + }, + error: (error) => { + console.log('error', error); + }, + }); } } else { this.passwordError = true; diff --git a/front-end/src/app/pages/my-account/my-account.component.html b/front-end/src/app/pages/my-account/my-account.component.html index 0f0a780..4632b32 100644 --- a/front-end/src/app/pages/my-account/my-account.component.html +++ b/front-end/src/app/pages/my-account/my-account.component.html @@ -1,15 +1,30 @@ diff --git a/front-end/src/app/pages/my-account/my-account.component.ts b/front-end/src/app/pages/my-account/my-account.component.ts index 257cbc5..57912ee 100644 --- a/front-end/src/app/pages/my-account/my-account.component.ts +++ b/front-end/src/app/pages/my-account/my-account.component.ts @@ -8,11 +8,13 @@ import { Router, RouterLink } from '@angular/router'; import { DeleteAccountDialog } from './dialogs/delete-account-dialog'; import { AuthService } from '../../services/auth.service'; import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar'; +import { CommonModule } from '@angular/common'; @Component({ selector: 'app-my-account', standalone: true, imports: [ + CommonModule, FormsModule, MatFormFieldModule, MatInputModule, diff --git a/front-end/src/app/pages/my-observations/my-observations.component.html b/front-end/src/app/pages/my-observations/my-observations.component.html index 76c3b06..39d8286 100644 --- a/front-end/src/app/pages/my-observations/my-observations.component.html +++ b/front-end/src/app/pages/my-observations/my-observations.component.html @@ -33,18 +33,21 @@
-
+
Mes observations
- @for (observation of myObservations; track observation.uuid) { + @for (observation of myObservations.features; track observation.id) { {{ - observation.name && observation.name !== "" - ? observation.name - : getEventType(observation.category)!.label + observation.properties.name && observation.properties.name !== "" + ? observation.properties.name + : getEventType(observation.properties.category)!.label }} {{ - observation.event_date | date: "dd/MM/yyyy" + observation.properties.event_date | date: "dd/MM/yyyy" }} -
- - - {{ area.offline ? "delete" : "download_for_offline" }} - - {{ area.name }} - - - } - +
+ + @for (area of areas; track area.id) { + +
+ +
+ + + {{ area.offline ? "delete" : "download_for_offline" }} + + {{ area.name }} + +
+ } +
+
diff --git a/front-end/src/app/pages/my-offline-data/my-offline-data.component.scss b/front-end/src/app/pages/my-offline-data/my-offline-data.component.scss index c52bbe0..263e7c0 100644 --- a/front-end/src/app/pages/my-offline-data/my-offline-data.component.scss +++ b/front-end/src/app/pages/my-offline-data/my-offline-data.component.scss @@ -26,12 +26,11 @@ mat-card { } mat-grid-list { - margin-top: 24px; mat-grid-tile { overflow: visible; } } -.information-container { - padding: 16px; +.areas-container { + padding-top: 24px; } diff --git a/front-end/src/app/pages/my-offline-data/my-offline-data.component.ts b/front-end/src/app/pages/my-offline-data/my-offline-data.component.ts index 6bbcbd9..2d05a0a 100644 --- a/front-end/src/app/pages/my-offline-data/my-offline-data.component.ts +++ b/front-end/src/app/pages/my-offline-data/my-offline-data.component.ts @@ -11,9 +11,9 @@ import { MatDialog } from '@angular/material/dialog'; import { DownloadDialog } from './dialogs/download-dialog'; import { DeleteDialog } from './dialogs/delete-dialog'; -import areas from '../../../data/areas.json'; import { OfflineService } from '../../services/offline.service'; import { LoaderDialog } from './dialogs/loader-dialog'; +import { SettingsService } from '../../services/settings.service'; @Component({ selector: 'app-my-offline-data', @@ -32,6 +32,7 @@ import { LoaderDialog } from './dialogs/loader-dialog'; export class MyOfflineDataComponent { readonly dialog = inject(MatDialog); offlineService = inject(OfflineService); + settingsService = inject(SettingsService); areas: any[] = []; @@ -79,8 +80,12 @@ export class MyOfflineDataComponent { } async initAreas() { - for (let index = 0; index < areas.length; index++) { - const area = areas[index]; + for ( + let index = 0; + index < this.settingsService.settings.value!.areas.length; + index++ + ) { + const area = this.settingsService.settings.value!.areas[index]; this.areas.push({ ...area, offline: Boolean( diff --git a/front-end/src/app/pages/new-observation/new-observation.component.html b/front-end/src/app/pages/new-observation/new-observation.component.html index 87ba9da..18db127 100644 --- a/front-end/src/app/pages/new-observation/new-observation.component.html +++ b/front-end/src/app/pages/new-observation/new-observation.component.html @@ -23,7 +23,10 @@ track observationsType.id ) { -
+
- + @for ( observation of currentObservationsFeatureCollection.features; - track observation.properties.uuid + track observation.id ) { diff --git a/front-end/src/app/pages/synthesis-interface/synthesis-interface.component.scss b/front-end/src/app/pages/synthesis-interface/synthesis-interface.component.scss index 2232f41..2eabdc5 100644 --- a/front-end/src/app/pages/synthesis-interface/synthesis-interface.component.scss +++ b/front-end/src/app/pages/synthesis-interface/synthesis-interface.component.scss @@ -7,6 +7,10 @@ $panel-height: 100vh; position: absolute; bottom: calc(16px + 48px + 56px); z-index: 400; + pointer-events: none; + button { + pointer-events: all; + } } #map { diff --git a/front-end/src/app/pages/synthesis-interface/synthesis-interface.component.ts b/front-end/src/app/pages/synthesis-interface/synthesis-interface.component.ts index cbfbb54..c0cd9ff 100644 --- a/front-end/src/app/pages/synthesis-interface/synthesis-interface.component.ts +++ b/front-end/src/app/pages/synthesis-interface/synthesis-interface.component.ts @@ -19,11 +19,10 @@ import { import { CommonModule } from '@angular/common'; import { MatListModule } from '@angular/material/list'; import moment from 'moment'; -import { Observations } from '../../types/types'; +import { observationsFeatureCollection } from '../../types/types'; -import evenementsRemarquables from '../../../data/evenements_remarquables.json'; -import observationTypes from '../../../data/types.json'; import { ObservationsService } from '../../services/observations.service'; +import { SettingsService } from '../../services/settings.service'; @Component({ selector: 'app-synthesis-interface', @@ -50,15 +49,17 @@ export class SynthesisInterfaceComponent { L: any; map: any; - observationsFeatureCollection: { features: Observations } = - evenementsRemarquables as any; - currentObservationsFeatureCollection: any = evenementsRemarquables; - observationsFeatureCollectionFiltered: any = evenementsRemarquables; + observationsFeatureCollection: observationsFeatureCollection | null = null; + currentObservationsFeatureCollection: observationsFeatureCollection | null = + null; + observationsFeatureCollectionFiltered: observationsFeatureCollection | null = + null; observationsLayer: L.GeoJSON | null = null; observationsClusterGroup: any; router = inject(Router); observationsService = inject(ObservationsService); + settingsService = inject(SettingsService); expansionPanelIsOpen = false; bounds: any; @@ -76,14 +77,6 @@ export class SynthesisInterfaceComponent { } async initMap() { - this.observationsService.getObservations().subscribe({ - next: (success) => { - console.log('success', success); - }, - error: (error) => { - console.log('error', error); - }, - }); this.L = await import('leaflet'); await import('leaflet.locatecontrol'); await import('leaflet.markercluster'); @@ -102,49 +95,62 @@ export class SynthesisInterfaceComponent { .locate({ setView: 'once', showPopup: false }) .addTo(this.map); - this.observationsLayer = this.L.default.geoJSON( - this.observationsFeatureCollection, - { - pointToLayer: (geoJsonPoint: any, latlng: any) => - this.L.default.marker(latlng, { - icon: this.L.default.divIcon({ - html: `
- -
`, - className: 'observation-marker', - iconSize: 32, - iconAnchor: [18, 28], - } as any), - autoPanOnFocus: false, - } as any), - onEachFeature: (geoJsonPoint: any, layer: any) => { - layer.once('click', () => { - this.handleObservationPopup(geoJsonPoint, layer); - }); - }, - }, - ); + this.observationsService.getObservations().subscribe({ + next: (success: any) => { + console.log('success', success); + this.ngZone.run(() => { + this.observationsFeatureCollection = success; + this.currentObservationsFeatureCollection = success; + this.observationsFeatureCollectionFiltered = success; + }); + this.observationsLayer = this.L.default.geoJSON( + this.observationsFeatureCollection, + { + pointToLayer: (geoJsonPoint: any, latlng: any) => + this.L.default.marker(latlng, { + icon: this.L.default.divIcon({ + html: `
+ +
`, + className: 'observation-marker', + iconSize: 32, + iconAnchor: [18, 28], + } as any), + autoPanOnFocus: false, + } as any), + onEachFeature: (geoJsonPoint: any, layer: any) => { + layer.once('click', () => { + this.handleObservationPopup(geoJsonPoint, layer); + }); + }, + }, + ); - this.observationsClusterGroup = this.L.default.markerClusterGroup({ - showCoverageOnHover: false, - removeOutsideVisibleBounds: false, - iconCreateFunction: (cluster: any) => { - return this.L.default.divIcon({ - html: - '
' + - cluster.getChildCount() + - '
', - className: '', - iconSize: 48, - } as any); - }, - }); + this.observationsClusterGroup = this.L.default.markerClusterGroup({ + showCoverageOnHover: false, + removeOutsideVisibleBounds: false, + iconCreateFunction: (cluster: any) => { + return this.L.default.divIcon({ + html: + '
' + + cluster.getChildCount() + + '
', + className: '', + iconSize: 48, + } as any); + }, + }); - this.observationsClusterGroup.addLayer(this.observationsLayer); - this.map.addLayer(this.observationsClusterGroup); + this.observationsClusterGroup.addLayer(this.observationsLayer); + this.map.addLayer(this.observationsClusterGroup); - this.fitToCurrentObservations(); - this.map.on('moveend', this.handleObservationsWithinBoundsBind); + this.fitToCurrentObservations(); + this.map.on('moveend', this.handleObservationsWithinBoundsBind); + }, + error: (error) => { + console.log('error', error); + }, + }); } handleObservationPopup(geoJsonPoint: any, layer?: any) { @@ -174,7 +180,7 @@ export class SynthesisInterfaceComponent { observationButton.className = 'observation-button'; observationButton.onclick = () => { const slug = slugify( - `${geoJsonPoint.properties.uuid}-${geoJsonPoint.properties.name}`, + `${geoJsonPoint.id}-${geoJsonPoint.properties.name}`, ); this.router.navigate(['/detail-d-une-observation', slug]); }; @@ -225,10 +231,11 @@ export class SynthesisInterfaceComponent { ) { this.filter.observationTypes = result.filter.observationTypes; observationFeatures = - this.observationsFeatureCollection.features.filter((feature: any) => - result.filter.observationTypes - .map((observationType: any) => observationType.id) - .includes(feature.properties.id_event_type), + this.observationsFeatureCollection?.features.filter( + (feature: any) => + result.filter.observationTypes + .map((observationType: any) => observationType.id) + .includes(feature.properties.id_event_type), ); } if ( @@ -251,7 +258,7 @@ export class SynthesisInterfaceComponent { ); } else { observationFeatures = - this.observationsFeatureCollection.features.filter( + this.observationsFeatureCollection?.features.filter( (observationFeature: any) => moment(observationFeature.properties.date_event).isBetween( result.filter.observationDates.start, @@ -263,7 +270,7 @@ export class SynthesisInterfaceComponent { } } this.observationsFeatureCollectionFiltered = { - ...this.observationsFeatureCollection, + ...this.observationsFeatureCollection!, features: observationFeatures || [], }; } else { @@ -290,9 +297,9 @@ export class SynthesisInterfaceComponent { } fitToCurrentObservations() { - if (this.observationsFeatureCollectionFiltered.features.length > 0) { + if (this.observationsFeatureCollectionFiltered!.features.length > 0) { this.bounds = this.L.default.latLngBounds( - this.observationsFeatureCollectionFiltered.features.map( + this.observationsFeatureCollectionFiltered!.features.map( (feature: any) => [ feature.geometry.coordinates[1], feature.geometry.coordinates[0], @@ -307,8 +314,8 @@ export class SynthesisInterfaceComponent { handleObservationsWithinBounds() { this.ngZone.run(() => { this.currentObservationsFeatureCollection = { - ...this.currentObservationsFeatureCollection, - features: this.observationsFeatureCollectionFiltered.features.filter( + ...this.currentObservationsFeatureCollection!, + features: this.observationsFeatureCollectionFiltered!.features.filter( (feature: any) => this.map .getBounds() @@ -337,15 +344,19 @@ export class SynthesisInterfaceComponent { updateMap() { this.observationsLayer!.clearLayers(); - this.observationsLayer!.addData(this.observationsFeatureCollectionFiltered); + this.observationsLayer!.addData( + this.observationsFeatureCollectionFiltered!, + ); this.observationsClusterGroup.clearLayers(); this.observationsClusterGroup.addLayer(this.observationsLayer); } getEventType(eventTypeId: number) { const eventTypes = [ - ...observationTypes.map((type) => type), - ...observationTypes.map((type) => type.children).flat(), + ...this.settingsService.settings.value!.categories.map((type) => type), + ...this.settingsService.settings + .value!.categories.map((type) => type.children) + .flat(), ]; return eventTypes.find( (observationType) => observationType.id === eventTypeId, diff --git a/front-end/src/app/services/auth.service.ts b/front-end/src/app/services/auth.service.ts index 2842387..de15d07 100644 --- a/front-end/src/app/services/auth.service.ts +++ b/front-end/src/app/services/auth.service.ts @@ -3,6 +3,8 @@ import { HttpClient, HttpHeaders } from '@angular/common/http'; import { inject } from '@angular/core'; import { BehaviorSubject } from 'rxjs'; import { environment } from '../../environments/environment'; +import { User } from '../types/types'; +import { OfflineService } from './offline.service'; const httpOptions = { headers: new HttpHeaders({ 'Content-Type': 'application/json' }), @@ -13,7 +15,10 @@ const httpOptions = { }) export class AuthService { httpClient = inject(HttpClient); + offlineService = inject(OfflineService); + isAuth = new BehaviorSubject(false); + user = new BehaviorSubject(null); checkAuth() { this.isAuth.next(localStorage.getItem('access_token') !== null); @@ -35,6 +40,7 @@ export class AuthService { } logout() { + this.offlineService.resetObservationsPending(); this.removeToken(); this.removeRefreshToken(); this.checkAuth(); @@ -60,15 +66,21 @@ export class AuthService { ); } - changePassword() {} + changePassword(password: string) { + return this.httpClient.patch( + `${environment.apiUrl}/api/accounts/me/`, + { password }, + httpOptions, + ); + } resetPassword() {} - refreshToken(token: string) { + refreshToken(refreshRoken: string) { return this.httpClient.post( `${environment.apiUrl}/api/token/refresh/`, { - refreshToken: token, + refresh: refreshRoken, }, httpOptions, ); @@ -97,4 +109,8 @@ export class AuthService { removeRefreshToken() { localStorage.removeItem('refresh_token'); } + + setUser(user: User) { + this.user.next(user); + } } diff --git a/front-end/src/app/services/observations.service.ts b/front-end/src/app/services/observations.service.ts index 48ddecf..99f202e 100644 --- a/front-end/src/app/services/observations.service.ts +++ b/front-end/src/app/services/observations.service.ts @@ -1,6 +1,7 @@ import { HttpClient, HttpHeaders } from '@angular/common/http'; import { inject, Injectable } from '@angular/core'; import { environment } from '../../environments/environment'; +import { Observation } from '../types/types'; const httpOptions = { headers: new HttpHeaders({ 'Content-Type': 'application/json' }), @@ -21,10 +22,21 @@ export class ObservationsService { ); } - getObservation(observationId: number) { + getObservation(observationId: string) { return this.httpClient.get( `${environment.apiUrl}/api/observations/${observationId}/`, httpOptions, ); } + + getMyObservations() { + return this.httpClient.get( + `${environment.apiUrl}/api/accounts/me/observations/`, + httpOptions, + ); + } + + sendObservation(observation: Observation) { + console.log('sendObservation'); + } } diff --git a/front-end/src/app/services/offline.service.ts b/front-end/src/app/services/offline.service.ts index f191259..9ed665b 100644 --- a/front-end/src/app/services/offline.service.ts +++ b/front-end/src/app/services/offline.service.ts @@ -39,6 +39,10 @@ const dbVersion = 1; export class OfflineService { observationsPending = new BehaviorSubject(null); + resetObservationsPending() { + this.observationsPending.next(null); + } + async handleObservationsPending() { const observationsPending = (await this.getAllDataInStore('observations')) .length; diff --git a/front-end/src/app/services/settings.service.ts b/front-end/src/app/services/settings.service.ts index b6816e2..daa166a 100644 --- a/front-end/src/app/services/settings.service.ts +++ b/front-end/src/app/services/settings.service.ts @@ -1,6 +1,8 @@ import { HttpClient, HttpHeaders } from '@angular/common/http'; import { inject, Injectable } from '@angular/core'; import { environment } from '../../environments/environment'; +import { BehaviorSubject } from 'rxjs'; +import { Settings } from '../types/types'; const httpOptions = { headers: new HttpHeaders({ 'Content-Type': 'application/json' }), @@ -11,6 +13,7 @@ const httpOptions = { }) export class SettingsService { httpClient = inject(HttpClient); + settings = new BehaviorSubject(null); constructor() {} @@ -20,4 +23,9 @@ export class SettingsService { httpOptions, ); } + + setSettings(settings: Settings) { + console.log('setSettings', settings); + this.settings.next(settings); + } } diff --git a/front-end/src/app/types/types.ts b/front-end/src/app/types/types.ts index 1112cf2..382c188 100644 --- a/front-end/src/app/types/types.ts +++ b/front-end/src/app/types/types.ts @@ -1,3 +1,16 @@ +export type observationsFeatureCollection = { + type: 'FeatureCollection'; + features: { + type: 'Feature'; + id: string; + geometry: { + type: 'Point'; + coordinates: number[]; + }; + properties: Observation; + }[]; +}; + export type Observations = Observation[]; export type Observation = { @@ -43,7 +56,7 @@ export type Area = { bbox: number[][]; }; -export type settings = { +export type Settings = { categories: ObservationTypes; areas: Areas; }; diff --git a/front-end/src/data/areas.json b/front-end/src/data/areas.json deleted file mode 100644 index 7b5fe36..0000000 --- a/front-end/src/data/areas.json +++ /dev/null @@ -1,56 +0,0 @@ -[ - { - "id": 1, - "name": "Zone A", - "description": "tooltip", - "bbox": [ - [45.933960441921585, 7.053222656250001], - [44.47299117260252, 5.921630859375001] - ] - }, - { - "id": 2, - "name": "Zone B", - "tooltip": "tooltip", - "bbox": [ - [45.933960441921585, 7.053222656250001], - [44.47299117260252, 5.921630859375001] - ] - }, - { - "id": 3, - "name": "Zone C", - "tooltip": "tooltip", - "bbox": [ - [45.933960441921585, 7.053222656250001], - [44.47299117260252, 5.921630859375001] - ] - }, - { - "id": 4, - "name": "Zone D", - "tooltip": "tooltip", - "bbox": [ - [45.933960441921585, 7.053222656250001], - [44.47299117260252, 5.921630859375001] - ] - }, - { - "id": 5, - "name": "Zone E", - "tooltip": "tooltip", - "bbox": [ - [45.933960441921585, 7.053222656250001], - [44.47299117260252, 5.921630859375001] - ] - }, - { - "id": 6, - "name": "Zone F", - "tooltip": "tooltip", - "bbox": [ - [45.933960441921585, 7.053222656250001], - [44.47299117260252, 5.921630859375001] - ] - } -] diff --git a/front-end/src/data/evenements_remarquables.json b/front-end/src/data/evenements_remarquables.json deleted file mode 100644 index e6e874f..0000000 --- a/front-end/src/data/evenements_remarquables.json +++ /dev/null @@ -1,51 +0,0 @@ -{ - "type": "FeatureCollection", - "features": [ - { - "type": "Feature", - "id": "Evenements_remarquables.21", - "bbox": [ - 6.410661006147566, 44.64683876379312, 6.410661006147566, - 44.64683876379312 - ], - "geometry": { - "type": "Point", - "coordinates": [6.410661006, 44.646838764] - }, - "properties": { - "uuid": 21, - "name": "Nom de l'événement", - "comments": "Fin d’été 2015. Date exacte non précisée. Disparition à la fin de l’été 2004 d’un névé permanent (avec plus de 3 mètres d’épaisseur de glace encore quelques années plus tôt) et création d’un lacquet. Suite à la création de cette marre en 2004 (10 mètres de diamètre et 2 mètres de profondeur), j’ai suivi pratiquement chaque année son évolution. Le lac a grossi un peu plus chaque année jusqu’à son maximum en 2015 (35 mètres de long sur 15 de large pour 5 mètres de profondeur estimée). Toutefois il y a eu plusieurs années ou le lac n’est pas apparu ( 2013, 2014 et 2016) suite à des cumuls de neige apportés par des avalanches de printemps très importantes (névé non fondu) et (ou) des étés moins chaud.", - "event_date": "2015-09-01", - "source": "source", - "category": 13, - "main_picture": { - "id": 1, - "uuid": "1", - "legend": "legend", - "media_file": "https://websig.ecrins-parcnational.fr/index.php/view/media/getMedia?repository=rgalt&project=rgalt&path=media/nouveau-lac-dent-02.jpg", - "media_type": "image", - "thumbnails": { - "small": "https://websig.ecrins-parcnational.fr/index.php/view/media/getMedia?repository=rgalt&project=rgalt&path=media/nouveau-lac-dent-02.jpg", - "medium": "https://websig.ecrins-parcnational.fr/index.php/view/media/getMedia?repository=rgalt&project=rgalt&path=media/nouveau-lac-dent-02.jpg", - "large": "https://websig.ecrins-parcnational.fr/index.php/view/media/getMedia?repository=rgalt&project=rgalt&path=media/nouveau-lac-dent-02.jpg" - } - }, - "medias": [ - { - "id": 1, - "uuid": "1", - "legend": "legend", - "media_file": "https://websig.ecrins-parcnational.fr/index.php/view/media/getMedia?repository=rgalt&project=rgalt&path=media/nouveau-lac-dent-02.jpg", - "media_type": "image", - "thumbnails": { - "small": "https://websig.ecrins-parcnational.fr/index.php/view/media/getMedia?repository=rgalt&project=rgalt&path=media/nouveau-lac-dent-02.jpg", - "medium": "https://websig.ecrins-parcnational.fr/index.php/view/media/getMedia?repository=rgalt&project=rgalt&path=media/nouveau-lac-dent-02.jpg", - "large": "https://websig.ecrins-parcnational.fr/index.php/view/media/getMedia?repository=rgalt&project=rgalt&path=media/nouveau-lac-dent-02.jpg" - } - } - ] - } - } - ] -} diff --git a/front-end/src/data/types.json b/front-end/src/data/types.json deleted file mode 100644 index 5d2ca9e..0000000 --- a/front-end/src/data/types.json +++ /dev/null @@ -1,94 +0,0 @@ -[ - { - "id": 1, - "label": "Avalanche", - "description": "description", - "pictogram": "question_mark", - "children": [ - { - "id": 13, - "label": "Chablis", - "pictogram": "question_mark", - "description": "description", - "children": [] - } - ] - }, - { - "id": 2, - "label": "Chablis", - "pictogram": "question_mark", - "description": "description", - "children": [] - }, - { - "id": 3, - "label": "Chute de blocs", - "pictogram": "question_mark", - "description": "description", - "children": [] - }, - { - "id": 4, - "label": "Crue torrentielle", - "pictogram": "question_mark", - "description": "description", - "children": [] - }, - { - "id": 5, - "label": "Incendie", - "pictogram": "question_mark", - "description": "description", - "children": [] - }, - { - "id": 6, - "label": "Inondation", - "pictogram": "question_mark", - "description": "description", - "children": [] - }, - { - "id": 7, - "label": "Lave torrentielle", - "pictogram": "question_mark", - "description": "description", - "children": [] - }, - { - "id": 8, - "label": "Ravinement", - "pictogram": "question_mark", - "description": "description", - "children": [] - }, - { - "id": 9, - "label": "Volis", - "pictogram": "question_mark", - "description": "description", - "children": [] - }, - { - "id": 10, - "label": "Apparition d'un nouveau lac", - "pictogram": "question_mark", - "description": "description", - "children": [] - }, - { - "id": 11, - "label": "Glissement de terrain", - "pictogram": "question_mark", - "description": "description", - "children": [] - }, - { - "id": 12, - "label": "Phénomène glaciaire", - "pictogram": "question_mark", - "description": "description", - "children": [] - } -] From e52383bc7b0ee4ae5c17fac0a7bb0cee35f357a5 Mon Sep 17 00:00:00 2001 From: Bastyen Date: Mon, 15 Jul 2024 17:20:23 +0200 Subject: [PATCH 03/12] send observation --- .../src/app/interceptors/http.interceptor.ts | 8 ++- .../src/app/pages/home/home.component.html | 5 +- .../my-observations.component.html | 2 +- .../my-observations.component.ts | 65 +++++++++++++---- .../my-offline-data.component.scss | 6 +- .../new-observation.component.scss | 2 +- .../new-observation.component.ts | 72 ++++++++++++++++--- .../observation-detail.component.html | 4 +- .../synthesis-interface.component.html | 2 +- .../synthesis-interface.component.scss | 2 +- .../synthesis-interface.component.ts | 6 +- .../src/app/services/observations.service.ts | 10 ++- front-end/src/app/types/types.ts | 28 ++++---- 13 files changed, 162 insertions(+), 50 deletions(-) diff --git a/front-end/src/app/interceptors/http.interceptor.ts b/front-end/src/app/interceptors/http.interceptor.ts index 8a94243..53a354a 100644 --- a/front-end/src/app/interceptors/http.interceptor.ts +++ b/front-end/src/app/interceptors/http.interceptor.ts @@ -39,7 +39,11 @@ export class HttpRequestInterceptor implements HttpInterceptor { return next.handle(req).pipe( catchError((error) => { - if (error instanceof HttpErrorResponse && error.status === 401) { + if ( + error instanceof HttpErrorResponse && + !req.url.includes('token') && + error.status === 401 + ) { return this.handle401Error(req, next); } return throwError(() => new Error('Erreur')); @@ -59,6 +63,8 @@ export class HttpRequestInterceptor implements HttpInterceptor { switchMap((token: any) => { this.isRefreshing = false; this.authService.saveToken(token.access); + this.refreshTokenSubject.next(token.access); + return next.handle(this.addTokenHeader(req, token.access)); }), catchError(() => { diff --git a/front-end/src/app/pages/home/home.component.html b/front-end/src/app/pages/home/home.component.html index 82844db..fcb563d 100644 --- a/front-end/src/app/pages/home/home.component.html +++ b/front-end/src/app/pages/home/home.component.html @@ -1,7 +1,8 @@
Date : {{ observationData.properties.event_date }} + >{{ + observationData.properties.event_date | date: "dd/MM/yyyy" + }}
Source : + + diff --git a/front-end/src/app/pages/my-observations/dialogs/my-observation-loader-dialog.scss b/front-end/src/app/pages/my-observations/dialogs/my-observation-loader-dialog.scss new file mode 100644 index 0000000..4715f66 --- /dev/null +++ b/front-end/src/app/pages/my-observations/dialogs/my-observation-loader-dialog.scss @@ -0,0 +1,4 @@ +.spinner-container { + display: flex; + justify-content: center; +} diff --git a/front-end/src/app/pages/my-observations/dialogs/my-observation-loader-dialog.ts b/front-end/src/app/pages/my-observations/dialogs/my-observation-loader-dialog.ts new file mode 100644 index 0000000..484c91b --- /dev/null +++ b/front-end/src/app/pages/my-observations/dialogs/my-observation-loader-dialog.ts @@ -0,0 +1,31 @@ +import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; +import { MatButtonModule } from '@angular/material/button'; +import { + MatDialogActions, + MatDialogClose, + MatDialogContent, + MatDialogRef, + MatDialogTitle, + MAT_DIALOG_DATA, +} from '@angular/material/dialog'; +import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; + +@Component({ + selector: 'my-observation-loader-dialog', + templateUrl: 'my-observation-loader-dialog.html', + styleUrl: './my-observation-loader-dialog.scss', + standalone: true, + imports: [ + MatButtonModule, + MatDialogActions, + MatDialogClose, + MatDialogTitle, + MatDialogContent, + MatProgressSpinnerModule, + ], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class MyObservationLoaderDialog { + dialogRef = inject(MatDialogRef); + data = inject<{ title: string }>(MAT_DIALOG_DATA); +} diff --git a/front-end/src/app/pages/my-observations/my-observations.component.ts b/front-end/src/app/pages/my-observations/my-observations.component.ts index 34830e9..6a95945 100644 --- a/front-end/src/app/pages/my-observations/my-observations.component.ts +++ b/front-end/src/app/pages/my-observations/my-observations.component.ts @@ -19,8 +19,9 @@ import { import { ObservationsService } from '../../services/observations.service'; import { SettingsService } from '../../services/settings.service'; import { OfflineService } from '../../services/offline.service'; - -const moment = _rollupMoment || _moment; +import { firstValueFrom } from 'rxjs'; +import { MyObservationLoaderDialog } from './dialogs/my-observation-loader-dialog'; +import { MatDialog } from '@angular/material/dialog'; @Component({ selector: 'app-my-observations', @@ -47,6 +48,8 @@ export class MyObservationsComponent { slugify = slugify; + readonly dialog = inject(MatDialog); + async ngOnInit() { await this.getMyOfflineObservations(); this.observationsService.getMyObservations().subscribe({ @@ -66,6 +69,14 @@ export class MyObservationsComponent { } async sendObservation(myOfflineObservation: Observation) { + const newObservationLoaderDialogRef = this.dialog.open( + MyObservationLoaderDialog, + { + width: '250px', + data: { title: 'Téléchargement en cours' }, + disableClose: true, + }, + ); const observation: ObservationFeature = { type: 'Feature', geometry: { @@ -82,7 +93,21 @@ export class MyObservationsComponent { observation.properties.name = myOfflineObservation.name; } this.observationsService.sendObservation(observation).subscribe({ - next: async () => { + next: async (observationResponse: any) => { + for ( + let index = 0; + index < myOfflineObservation.files!.length; + index++ + ) { + const file = myOfflineObservation.files![index]; + await firstValueFrom( + this.observationsService.sendPhotoObservation( + observationResponse.id, + file, + ), + ); + } + newObservationLoaderDialogRef.close(); this.snackBar.open( `Observation "${observation.properties.name ? observation.properties.name : this.getEventType(observation.properties.category)?.label}" transférée`, '', diff --git a/front-end/src/app/pages/new-observation/dialogs/new-observation-loader-dialog.html b/front-end/src/app/pages/new-observation/dialogs/new-observation-loader-dialog.html new file mode 100644 index 0000000..9d8c9a6 --- /dev/null +++ b/front-end/src/app/pages/new-observation/dialogs/new-observation-loader-dialog.html @@ -0,0 +1,4 @@ +

Transfert en cours

+ + + diff --git a/front-end/src/app/pages/new-observation/dialogs/new-observation-loader-dialog.scss b/front-end/src/app/pages/new-observation/dialogs/new-observation-loader-dialog.scss new file mode 100644 index 0000000..4715f66 --- /dev/null +++ b/front-end/src/app/pages/new-observation/dialogs/new-observation-loader-dialog.scss @@ -0,0 +1,4 @@ +.spinner-container { + display: flex; + justify-content: center; +} diff --git a/front-end/src/app/pages/new-observation/dialogs/new-observation-loader-dialog.ts b/front-end/src/app/pages/new-observation/dialogs/new-observation-loader-dialog.ts new file mode 100644 index 0000000..1150071 --- /dev/null +++ b/front-end/src/app/pages/new-observation/dialogs/new-observation-loader-dialog.ts @@ -0,0 +1,31 @@ +import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; +import { MatButtonModule } from '@angular/material/button'; +import { + MatDialogActions, + MatDialogClose, + MatDialogContent, + MatDialogRef, + MatDialogTitle, + MAT_DIALOG_DATA, +} from '@angular/material/dialog'; +import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; + +@Component({ + selector: 'new-observation-loader-dialog', + templateUrl: 'new-observation-loader-dialog.html', + styleUrl: './new-observation-loader-dialog.scss', + standalone: true, + imports: [ + MatButtonModule, + MatDialogActions, + MatDialogClose, + MatDialogTitle, + MatDialogContent, + MatProgressSpinnerModule, + ], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class NewObservationLoaderDialog { + dialogRef = inject(MatDialogRef); + data = inject<{ title: string }>(MAT_DIALOG_DATA); +} diff --git a/front-end/src/app/pages/new-observation/new-observation.component.html b/front-end/src/app/pages/new-observation/new-observation.component.html index 18db127..7ea6908 100644 --- a/front-end/src/app/pages/new-observation/new-observation.component.html +++ b/front-end/src/app/pages/new-observation/new-observation.component.html @@ -98,8 +98,20 @@ Insérez une photo depuis la galerie
-
- +
+ @for (photo of photoForm.value.photos; track photo.file.name) { +
+ + +
+ }
@@ -109,7 +121,7 @@ Localisez mon observation
-
+
Latitude : {{ mapForm.value.position.lat }}
Longitude : {{ mapForm.value.position.lng }}
@@ -163,13 +175,17 @@ {{ typeForm.value.type.label }}
-
+
Contenu multimédia
-
- +
+ @for (photo of photoForm.value.photos; track photo) { +
+ +
+ }
-
+
Localisation
Latitude : {{ mapForm.value.position.lat }}
Longitude : {{ mapForm.value.position.lng }}
diff --git a/front-end/src/app/pages/new-observation/new-observation.component.scss b/front-end/src/app/pages/new-observation/new-observation.component.scss index b65d2ee..36c7362 100644 --- a/front-end/src/app/pages/new-observation/new-observation.component.scss +++ b/front-end/src/app/pages/new-observation/new-observation.component.scss @@ -30,11 +30,23 @@ mat-card { gap: 16px; } -.photo-container { - img { - width: 200px; - height: 200px; - object-fit: contain; +.photos-container { + display: flex; + overflow-x: scroll; + .photo-container { + position: relative; + .delete-photo-button { + position: absolute; + right: 0; + } + .photo { + width: 200px; + max-height: 200px; + object-fit: contain; + } + &:not(:last-of-type) { + margin-right: 16px; + } } } diff --git a/front-end/src/app/pages/new-observation/new-observation.component.ts b/front-end/src/app/pages/new-observation/new-observation.component.ts index 301d75b..2296ed9 100644 --- a/front-end/src/app/pages/new-observation/new-observation.component.ts +++ b/front-end/src/app/pages/new-observation/new-observation.component.ts @@ -43,6 +43,9 @@ import { v4 as uuidv4 } from 'uuid'; import { SettingsService } from '../../services/settings.service'; import { ObservationsService } from '../../services/observations.service'; import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar'; +import { firstValueFrom } from 'rxjs'; +import { NewObservationLoaderDialog } from './dialogs/new-observation-loader-dialog'; +import { MatDialog } from '@angular/material/dialog'; const moment = _rollupMoment || _moment; @@ -95,9 +98,9 @@ export class NewObservationComponent { }); photoForm: FormGroup<{ - photo: FormControl; + photos: FormControl<{ file: File; objectUrl: string }[] | null>; }> = new FormGroup({ - photo: new FormControl(null), + photos: new FormControl([]), }); mapForm: FormGroup<{ @@ -124,6 +127,7 @@ export class NewObservationComponent { observationsService = inject(ObservationsService); snackBar = inject(MatSnackBar); ngZone = inject(NgZone); + readonly dialog = inject(MatDialog); observationsTypes: ObservationTypes = this.settingsService.settings.value!.categories; @@ -222,11 +226,20 @@ export class NewObservationComponent { } onPhotoSelected(event: any) { - const photo: File = event.target.files[0]; - - if (photo) { - this.photoForm.value.photo = URL.createObjectURL(photo); + const photoSelected: File = event.target.files[0]; + if ( + photoSelected && + this.photoForm.value.photos && + !this.photoForm.value.photos.find( + (photo) => photo.file.name === photoSelected.name, + ) + ) { + this.photoForm.value.photos.push({ + file: photoSelected, + objectUrl: URL.createObjectURL(photoSelected), + } as any); } + event.target.value = null; } backToPreviousObservations() { @@ -249,6 +262,7 @@ export class NewObservationComponent { this.mapForm.value.position!.lng, this.mapForm.value.position!.lng, ], + files: this.photoForm.value.photos!.map((photo) => photo.file), }; await this.offlineService.writeOrUpdateDataInStore('observations', [ newObservation, @@ -258,6 +272,14 @@ export class NewObservationComponent { } sendObservation() { + const newObservationLoaderDialogRef = this.dialog.open( + NewObservationLoaderDialog, + { + width: '250px', + data: { title: 'Téléchargement en cours' }, + disableClose: true, + }, + ); const observation: ObservationFeature = { type: 'Feature', geometry: { @@ -279,11 +301,26 @@ export class NewObservationComponent { observation.properties.name = this.moreDataForm.value.name!; } this.observationsService.sendObservation(observation).subscribe({ - next: () => { + next: async (observationResponse: any) => { + for ( + let index = 0; + index < this.photoForm.value.photos!.length; + index++ + ) { + const photo = this.photoForm.value.photos![index]; + await firstValueFrom( + this.observationsService.sendPhotoObservation( + observationResponse.id, + photo.file, + ), + ); + } + newObservationLoaderDialogRef.close(); this.snackBar.open('Observation transférée', '', { duration: 2000 }); this.router.navigate(['/']); }, error: async () => { + newObservationLoaderDialogRef.close(); this.snackBar.open( "Une erreur est survenue lors du transfert de l'observation", '', @@ -291,7 +328,6 @@ export class NewObservationComponent { duration: 2000, }, ); - await this.saveAsDraft(); }, }); @@ -308,4 +344,13 @@ export class NewObservationComponent { (observationType) => observationType.id === eventTypeId, ); } + + deletePhoto(selectedPhoto: any) { + this.photoForm.value.photos!.splice( + this.photoForm.value.photos!.findIndex( + (photo) => photo.file.name === selectedPhoto.file.name, + ), + 1, + ); + } } diff --git a/front-end/src/app/services/observations.service.ts b/front-end/src/app/services/observations.service.ts index 9d8d0f4..c822162 100644 --- a/front-end/src/app/services/observations.service.ts +++ b/front-end/src/app/services/observations.service.ts @@ -1,12 +1,16 @@ import { HttpClient, HttpHeaders } from '@angular/common/http'; import { inject, Injectable } from '@angular/core'; import { environment } from '../../environments/environment'; -import { Observation, ObservationFeature } from '../types/types'; +import { ObservationFeature } from '../types/types'; const httpOptions = { headers: new HttpHeaders({ 'Content-Type': 'application/json' }), }; +const httpMediaOptions = { + headers: new HttpHeaders({ 'Content-Type': 'multipart/form-data' }), +}; + @Injectable({ providedIn: 'root', }) @@ -43,4 +47,15 @@ export class ObservationsService { httpOptions, ); } + + sendPhotoObservation(observationId: any, file: any) { + console.log('sendPhotoObservation', observationId, file); + const formData = new FormData(); + formData.append('media_file', file); + formData.append('media_type', 'image'); + return this.httpClient.post( + `${environment.apiUrl}/api/accounts/me/observations/${observationId}/medias/`, + formData, + ); + } } diff --git a/front-end/src/app/types/types.ts b/front-end/src/app/types/types.ts index 4ae2493..28457a4 100644 --- a/front-end/src/app/types/types.ts +++ b/front-end/src/app/types/types.ts @@ -26,6 +26,7 @@ export type Observation = { main_picture?: Picture; medias?: Picture[]; coordinates?: number[]; + files?: File[]; }; export type Picture = { From 1b4d6c6274c140f81c57f7db71edee86f2e89f22 Mon Sep 17 00:00:00 2001 From: Bastyen Date: Tue, 16 Jul 2024 16:56:56 +0200 Subject: [PATCH 05/12] handle offline settings --- front-end/src/app/app.component.ts | 9 +++- .../new-observation.component.html | 4 +- .../new-observation.component.ts | 28 ++++++++++-- front-end/src/app/services/offline.service.ts | 30 +++++++++++-- .../src/app/services/settings.service.ts | 43 +++++++++++++++++-- front-end/src/app/types/types.ts | 14 ++++++ 6 files changed, 114 insertions(+), 14 deletions(-) diff --git a/front-end/src/app/app.component.ts b/front-end/src/app/app.component.ts index f6fd104..ff67e5c 100644 --- a/front-end/src/app/app.component.ts +++ b/front-end/src/app/app.component.ts @@ -162,8 +162,13 @@ export class AppComponent { this.handleAuthentification(value); }); - this.settingsService.getSettings().subscribe((settings: any) => { - this.settingsService.setSettings(settings); + this.settingsService.getSettings().subscribe({ + next: async (settings: any) => { + await this.settingsService.setSettings(settings); + }, + error: async () => { + await this.settingsService.useOfflineSettings(); + }, }); this.router.events.subscribe((event) => { diff --git a/front-end/src/app/pages/new-observation/new-observation.component.html b/front-end/src/app/pages/new-observation/new-observation.component.html index 7ea6908..2c2d704 100644 --- a/front-end/src/app/pages/new-observation/new-observation.component.html +++ b/front-end/src/app/pages/new-observation/new-observation.component.html @@ -49,9 +49,9 @@ }" > {{ observationsType.label }} diff --git a/front-end/src/app/pages/new-observation/new-observation.component.ts b/front-end/src/app/pages/new-observation/new-observation.component.ts index 2296ed9..81ca1f7 100644 --- a/front-end/src/app/pages/new-observation/new-observation.component.ts +++ b/front-end/src/app/pages/new-observation/new-observation.component.ts @@ -33,6 +33,8 @@ import { default as _rollupMoment } from 'moment'; import { round } from '@turf/helpers'; import { + Icon, + Icons, Observation, ObservationFeature, ObservationType, @@ -129,8 +131,9 @@ export class NewObservationComponent { ngZone = inject(NgZone); readonly dialog = inject(MatDialog); - observationsTypes: ObservationTypes = - this.settingsService.settings.value!.categories; + observationsTypes: ObservationTypes = []; + + icons: Icons = []; ngOnInit() { this.mobile = this.platform.ANDROID || this.platform.IOS; @@ -157,10 +160,17 @@ export class NewObservationComponent { } } }); + + this.settingsService.settings.subscribe((settings) => { + if (settings) { + this.observationsTypes = settings.categories; + } + }); } constructor() { afterNextRender(async () => { - this.initMap(); + await this.initIcons(); + await this.initMap(); }); } @@ -353,4 +363,16 @@ export class NewObservationComponent { 1, ); } + + async initIcons() { + const icons = await this.offlineService.getAllDataInStore('icons'); + this.icons = icons.map((icon) => ({ + ...icon, + objectUrl: URL.createObjectURL(icon.file), + })); + } + + getIconFromStorage(iconId: number) { + return this.icons.find((icon) => icon.id === iconId)!.objectUrl; + } } diff --git a/front-end/src/app/services/offline.service.ts b/front-end/src/app/services/offline.service.ts index 9ed665b..1e7abd9 100644 --- a/front-end/src/app/services/offline.service.ts +++ b/front-end/src/app/services/offline.service.ts @@ -1,6 +1,14 @@ import { Injectable } from '@angular/core'; import { DBSchema, openDB } from 'idb'; -import { Area, Areas, Observation, Observations } from '../types/types'; +import { + Area, + Areas, + Icon, + Icons, + Observation, + Observations, + OfflineSettings, +} from '../types/types'; import { BehaviorSubject } from 'rxjs'; import { TileInfo, TileLayerOffline } from 'leaflet.offline'; @@ -8,7 +16,7 @@ type ObjectStores = ObjectStore[]; type ObjectStore = { name: ObjectStoresName; keyPath: KeyPath }; -type ObjectStoresName = 'observations' | 'offline-areas'; +type ObjectStoresName = 'observations' | 'offline-areas' | 'settings' | 'icons'; type KeyPath = 'uuid' | 'id'; @@ -16,9 +24,13 @@ type ObjectStoresType = T extends 'observations' ? Observation : T extends 'offline-areas' ? Area - : never; + : T extends 'settings' + ? OfflineSettings + : T extends 'icons' + ? Icon + : never; -type ObjectStoresData = Observations | Areas; +type ObjectStoresData = Observations | Areas | OfflineSettings[] | Icons; interface DB extends DBSchema { observations: { @@ -29,6 +41,14 @@ interface DB extends DBSchema { value: Area; key: number; }; + settings: { + value: OfflineSettings; + key: number; + }; + icons: { + value: Icon; + key: number; + }; } const dbVersion = 1; @@ -61,6 +81,8 @@ export class OfflineService { const objectStoresNames: ObjectStores = [ { name: 'observations', keyPath: 'uuid' }, { name: 'offline-areas', keyPath: 'id' }, + { name: 'settings', keyPath: 'id' }, + { name: 'icons', keyPath: 'id' }, ]; objectStoresNames.forEach((objectStoresName) => { diff --git a/front-end/src/app/services/settings.service.ts b/front-end/src/app/services/settings.service.ts index daa166a..752e28d 100644 --- a/front-end/src/app/services/settings.service.ts +++ b/front-end/src/app/services/settings.service.ts @@ -1,19 +1,25 @@ import { HttpClient, HttpHeaders } from '@angular/common/http'; import { inject, Injectable } from '@angular/core'; import { environment } from '../../environments/environment'; -import { BehaviorSubject } from 'rxjs'; +import { BehaviorSubject, firstValueFrom } from 'rxjs'; import { Settings } from '../types/types'; +import { OfflineService } from './offline.service'; const httpOptions = { headers: new HttpHeaders({ 'Content-Type': 'application/json' }), }; +const httpIconOptions = { + headers: new HttpHeaders({ Accept: 'image/svg+xml' }), +}; + @Injectable({ providedIn: 'root', }) export class SettingsService { httpClient = inject(HttpClient); settings = new BehaviorSubject(null); + offlineService = inject(OfflineService); constructor() {} @@ -24,8 +30,39 @@ export class SettingsService { ); } - setSettings(settings: Settings) { - console.log('setSettings', settings); + async setSettings(settings: Settings) { + await this.offlineService.writeOrUpdateDataInStore('settings', [ + { + id: 1, + settings, + }, + ]); + for (let index = 0; index < settings.categories.length; index++) { + const category = settings.categories[index]; + if (!(await this.offlineService.getDataInStore('icons', category.id))) { + const file: any = await firstValueFrom( + this.getIcon(category.pictogram), + ); + await this.offlineService.writeOrUpdateDataInStore('icons', [ + { id: category.id, pictogram: category.pictogram, file }, + ]); + } + } + this.settings.next(settings); + } + + async useOfflineSettings() { + const { settings } = await this.offlineService.getDataInStore( + 'settings', + 1, + ); this.settings.next(settings); } + + getIcon(url: any) { + return this.httpClient.get(url, { + ...httpIconOptions, + responseType: 'blob', + }); + } } diff --git a/front-end/src/app/types/types.ts b/front-end/src/app/types/types.ts index 28457a4..8c81f26 100644 --- a/front-end/src/app/types/types.ts +++ b/front-end/src/app/types/types.ts @@ -66,6 +66,11 @@ export type Settings = { areas: Areas; }; +export type OfflineSettings = { + id: number; + settings: Settings; +}; + export type User = { id: number; uuid: string; @@ -74,3 +79,12 @@ export type User = { last_name: string; first_name: string; }; + +export type Icons = Icon[]; + +export type Icon = { + id: number; + pictogram: string; + file: File; + objectUrl?: string; +}; From 22ad4dd608c3003d88745fd5eae1e8f4fccdbf6d Mon Sep 17 00:00:00 2001 From: Bastyen Date: Wed, 17 Jul 2024 10:40:52 +0200 Subject: [PATCH 06/12] handle map settings --- front-end/src/app/app.component.ts | 13 +++-- .../my-offline-data.component.ts | 24 ++++++--- .../new-observation.component.ts | 40 +++++++++++--- .../observation-detail.component.ts | 2 +- .../synthesis-interface.component.ts | 54 +++++++++++++++---- .../src/app/services/settings.service.ts | 1 + front-end/src/app/types/types.ts | 13 +++++ 7 files changed, 119 insertions(+), 28 deletions(-) diff --git a/front-end/src/app/app.component.ts b/front-end/src/app/app.component.ts index ff67e5c..5d40b0f 100644 --- a/front-end/src/app/app.component.ts +++ b/front-end/src/app/app.component.ts @@ -81,9 +81,16 @@ export class AppComponent { { id: 3, text: 'Saisir une nouvelle observation', - routerLink: 'nouvelle-observation', - authenficated: true, - click: () => null, + routerLink: null, + authenficated: null, + click: () => { + this.sidenav.close(); + this.router.navigate([ + this.authService.isAuth.value + ? '/nouvelle-observation' + : '/se-connecter', + ]); + }, observationsPending: false, }, { diff --git a/front-end/src/app/pages/my-offline-data/my-offline-data.component.ts b/front-end/src/app/pages/my-offline-data/my-offline-data.component.ts index 2d05a0a..f753e46 100644 --- a/front-end/src/app/pages/my-offline-data/my-offline-data.component.ts +++ b/front-end/src/app/pages/my-offline-data/my-offline-data.component.ts @@ -14,6 +14,8 @@ import { DeleteDialog } from './dialogs/delete-dialog'; import { OfflineService } from '../../services/offline.service'; import { LoaderDialog } from './dialogs/loader-dialog'; import { SettingsService } from '../../services/settings.service'; +import { environment } from '../../../environments/environment'; +import { Area } from '../../types/types'; @Component({ selector: 'app-my-offline-data', @@ -103,7 +105,7 @@ export class MyOfflineDataComponent { } } - openDownloadDialog(area: any) { + openDownloadDialog(area: Area) { const downloadDialogRef = this.dialog.open(DownloadDialog, { width: '250px', data: { name: area.name }, @@ -118,17 +120,25 @@ export class MyOfflineDataComponent { }); const { tileLayerOffline } = await import('leaflet.offline'); const L = await import('leaflet'); - const url = - 'https://data.geopf.fr/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=GEOGRAPHICALGRIDSYSTEMS.PLANIGNV2&STYLE=normal&FORMAT=image/png&TILEMATRIXSET=PM&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}'; - const attribution = "IGN"; - const minZoom = 0; - const maxZoom = 9; + const defaultLayer = this.settingsService.settings.value?.base_maps + .main_map.url + ? this.settingsService.settings.value?.base_maps.main_map.url + : environment.baseMaps.mainMap.url; + const defaultAttribution = this.settingsService.settings.value + ?.base_maps.main_map.attribution + ? this.settingsService.settings.value?.base_maps.main_map.attribution + : environment.baseMaps.mainMap.attribution; + const minZoom = area.min_zoom; + const maxZoom = area.max_zoom; const bounds = L.default.latLngBounds([ { lat: area.bbox[0][0], lng: area.bbox[0][1] }, { lat: area.bbox[1][0], lng: area.bbox[1][1] }, ]); - const offlineLayer = tileLayerOffline(url, { attribution }); + + const offlineLayer = tileLayerOffline(defaultLayer, { + attribution: defaultAttribution, + }); await this.offlineService.writeOrUpdateTilesInStore( offlineLayer, bounds, diff --git a/front-end/src/app/pages/new-observation/new-observation.component.ts b/front-end/src/app/pages/new-observation/new-observation.component.ts index 81ca1f7..9b5992b 100644 --- a/front-end/src/app/pages/new-observation/new-observation.component.ts +++ b/front-end/src/app/pages/new-observation/new-observation.component.ts @@ -48,6 +48,8 @@ import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar'; import { firstValueFrom } from 'rxjs'; import { NewObservationLoaderDialog } from './dialogs/new-observation-loader-dialog'; import { MatDialog } from '@angular/material/dialog'; +import { environment } from '../../../environments/environment'; +import { tileLayerOffline } from 'leaflet.offline'; const moment = _rollupMoment || _moment; @@ -180,12 +182,38 @@ export class NewObservationComponent { this.map = this.L.map('map', { zoom: 4, center: [47, 2] }); - this.L.tileLayer( - 'https://data.geopf.fr/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=GEOGRAPHICALGRIDSYSTEMS.PLANIGNV2&STYLE=normal&FORMAT=image/png&TILEMATRIXSET=PM&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}', - { - attribution: "IGN", - }, - ).addTo(this.map); + const defaultLayerUrl = this.settingsService.settings.value?.base_maps + .main_map.url + ? this.settingsService.settings.value?.base_maps.main_map.url + : environment.baseMaps.mainMap.url; + const defaultLayerAttribution = this.settingsService.settings.value + ?.base_maps.main_map.attribution + ? this.settingsService.settings.value?.base_maps.main_map.attribution + : environment.baseMaps.mainMap.attribution; + + const satelliteLayerUrl = this.settingsService.settings.value?.base_maps + .satellite_map.url + ? this.settingsService.settings.value?.base_maps.satellite_map.url + : environment.baseMaps.satellitMap.url; + const satelliteLayerAttribution = this.settingsService.settings.value + ?.base_maps.satellite_map.attribution + ? this.settingsService.settings.value?.base_maps.satellite_map.attribution + : environment.baseMaps.satellitMap.attribution; + + const defaultLayer = tileLayerOffline(defaultLayerUrl, { + attribution: defaultLayerAttribution, + }); + defaultLayer.addTo(this.map); + const satelliteLayer = tileLayerOffline(satelliteLayerUrl, { + attribution: satelliteLayerAttribution, + }); + this.L.control + .layers( + { Defaut: defaultLayer, Satellite: satelliteLayer }, + {}, + { collapsed: true }, + ) + .addTo(this.map); this.map.on('move', (e: any) => { const center = this.map.getCenter(); diff --git a/front-end/src/app/pages/observation-detail/observation-detail.component.ts b/front-end/src/app/pages/observation-detail/observation-detail.component.ts index 81810eb..d71f3ff 100644 --- a/front-end/src/app/pages/observation-detail/observation-detail.component.ts +++ b/front-end/src/app/pages/observation-detail/observation-detail.component.ts @@ -1,7 +1,7 @@ import { Component, inject, Input } from '@angular/core'; +import { CommonModule } from '@angular/common'; import { Observation, ObservationType } from '../../types/types'; -import { CommonModule } from '@angular/common'; import { ObservationsService } from '../../services/observations.service'; import { SettingsService } from '../../services/settings.service'; diff --git a/front-end/src/app/pages/synthesis-interface/synthesis-interface.component.ts b/front-end/src/app/pages/synthesis-interface/synthesis-interface.component.ts index 05e43e5..80b2935 100644 --- a/front-end/src/app/pages/synthesis-interface/synthesis-interface.component.ts +++ b/front-end/src/app/pages/synthesis-interface/synthesis-interface.component.ts @@ -23,6 +23,7 @@ import { observationsFeatureCollection } from '../../types/types'; import { ObservationsService } from '../../services/observations.service'; import { SettingsService } from '../../services/settings.service'; +import { environment } from '../../../environments/environment'; @Component({ selector: 'app-synthesis-interface', @@ -83,13 +84,38 @@ export class SynthesisInterfaceComponent { const { tileLayerOffline } = await import('leaflet.offline'); this.map = this.L.default.map('map', { zoom: 4, center: [47, 2] }); + const defaultLayerUrl = this.settingsService.settings.value?.base_maps + .main_map.url + ? this.settingsService.settings.value?.base_maps.main_map.url + : environment.baseMaps.mainMap.url; + const defaultLayerAttribution = this.settingsService.settings.value + ?.base_maps.main_map.attribution + ? this.settingsService.settings.value?.base_maps.main_map.attribution + : environment.baseMaps.mainMap.attribution; - tileLayerOffline( - 'https://data.geopf.fr/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=GEOGRAPHICALGRIDSYSTEMS.PLANIGNV2&STYLE=normal&FORMAT=image/png&TILEMATRIXSET=PM&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}', - { - attribution: "IGN", - }, - ).addTo(this.map); + const satelliteLayerUrl = this.settingsService.settings.value?.base_maps + .satellite_map.url + ? this.settingsService.settings.value?.base_maps.satellite_map.url + : environment.baseMaps.satellitMap.url; + const satelliteLayerAttribution = this.settingsService.settings.value + ?.base_maps.satellite_map.attribution + ? this.settingsService.settings.value?.base_maps.satellite_map.attribution + : environment.baseMaps.satellitMap.attribution; + + const defaultLayer = tileLayerOffline(defaultLayerUrl, { + attribution: defaultLayerAttribution, + }); + defaultLayer.addTo(this.map); + const satelliteLayer = tileLayerOffline(satelliteLayerUrl, { + attribution: satelliteLayerAttribution, + }); + this.L.control + .layers( + { Defaut: defaultLayer, Satellite: satelliteLayer }, + {}, + { collapsed: true }, + ) + .addTo(this.map); this.L.default.control .locate({ setView: 'once', showPopup: false }) @@ -106,18 +132,24 @@ export class SynthesisInterfaceComponent { this.observationsLayer = this.L.default.geoJSON( this.observationsFeatureCollection, { - pointToLayer: (geoJsonPoint: any, latlng: any) => - this.L.default.marker(latlng, { + pointToLayer: (geoJsonPoint: any, latlng: any) => { + const icon = this.getEventType(geoJsonPoint.properties.category); + return this.L.default.marker(latlng, { icon: this.L.default.divIcon({ - html: `
- + html: + icon && icon.pictogram + ? `
+ +
` + : `
`, className: 'observation-marker', iconSize: 32, iconAnchor: [18, 28], } as any), autoPanOnFocus: false, - } as any), + } as any); + }, onEachFeature: (geoJsonPoint: any, layer: any) => { layer.once('click', () => { this.handleObservationPopup(geoJsonPoint, layer); diff --git a/front-end/src/app/services/settings.service.ts b/front-end/src/app/services/settings.service.ts index 752e28d..5aea279 100644 --- a/front-end/src/app/services/settings.service.ts +++ b/front-end/src/app/services/settings.service.ts @@ -48,6 +48,7 @@ export class SettingsService { ]); } } + console.log(settings); this.settings.next(settings); } diff --git a/front-end/src/app/types/types.ts b/front-end/src/app/types/types.ts index 8c81f26..5e1d03b 100644 --- a/front-end/src/app/types/types.ts +++ b/front-end/src/app/types/types.ts @@ -59,11 +59,24 @@ export type Area = { name: string; description: string; bbox: number[][]; + min_zoom: number; + max_zoom: number; + offline?: boolean; }; export type Settings = { categories: ObservationTypes; areas: Areas; + base_maps: { + main_map: { + url: string; + attribution: string; + }; + satellite_map: { + url: string; + attribution: string; + }; + }; }; export type OfflineSettings = { From 334b30d89fc7aae5988caa9502116689117a9506 Mon Sep 17 00:00:00 2001 From: Bastyen Date: Wed, 17 Jul 2024 17:01:49 +0200 Subject: [PATCH 07/12] improve get observations --- front-end/src/app/app.component.html | 5 +- front-end/src/app/app.component.scss | 4 + .../src/app/interceptors/http.interceptor.ts | 2 +- .../src/app/pages/login/login.component.html | 5 ++ .../src/app/pages/login/login.component.scss | 4 + .../src/app/pages/login/login.component.ts | 8 +- .../my-observations.component.ts | 1 + .../new-observation.component.ts | 2 +- .../observation-detail.component.html | 18 +++-- .../observation-detail.component.scss | 7 +- .../synthesis-interface.component.scss | 2 +- .../synthesis-interface.component.ts | 80 ++++++------------- front-end/src/app/services/auth.service.ts | 5 +- .../src/app/services/observations.service.ts | 35 +++++--- .../src/app/services/settings.service.ts | 5 +- 15 files changed, 97 insertions(+), 86 deletions(-) diff --git a/front-end/src/app/app.component.html b/front-end/src/app/app.component.html index c580338..51b4115 100644 --- a/front-end/src/app/app.component.html +++ b/front-end/src/app/app.component.html @@ -22,7 +22,10 @@ {{ title }} - + new Error('Erreur')); + return throwError(() => error.error.detail); }), ); } diff --git a/front-end/src/app/pages/login/login.component.html b/front-end/src/app/pages/login/login.component.html index 9ab024c..c40e028 100644 --- a/front-end/src/app/pages/login/login.component.html +++ b/front-end/src/app/pages/login/login.component.html @@ -25,6 +25,11 @@ Un mot de passe est nécessaire } + @if (error) { + + }
diff --git a/front-end/src/app/pages/login/login.component.scss b/front-end/src/app/pages/login/login.component.scss index 9a8820d..a62b7c4 100644 --- a/front-end/src/app/pages/login/login.component.scss +++ b/front-end/src/app/pages/login/login.component.scss @@ -12,3 +12,7 @@ mat-form-field { max-width: 400px; padding: 16px; } + +.login-error-container { + padding: 0px 16px 16px 16px; +} diff --git a/front-end/src/app/pages/login/login.component.ts b/front-end/src/app/pages/login/login.component.ts index 598653e..27f5033 100644 --- a/front-end/src/app/pages/login/login.component.ts +++ b/front-end/src/app/pages/login/login.component.ts @@ -41,6 +41,8 @@ export class LoginComponent { authService = inject(AuthService); snackBar = inject(MatSnackBar); + error: String | null = null; + onLogin() { if (this.emailFormControl.valid && this.passwordFormControl.valid) { this.authService @@ -57,8 +59,10 @@ export class LoginComponent { this.authService.checkAuth(); this.router.navigate(['..']); }, - error: (error) => { - console.log('error', error); + error: (error: any) => { + console.log(error); + this.error = error.toString(); + console.log(this.error); }, }); } diff --git a/front-end/src/app/pages/my-observations/my-observations.component.ts b/front-end/src/app/pages/my-observations/my-observations.component.ts index 6a95945..af72c6e 100644 --- a/front-end/src/app/pages/my-observations/my-observations.component.ts +++ b/front-end/src/app/pages/my-observations/my-observations.component.ts @@ -77,6 +77,7 @@ export class MyObservationsComponent { disableClose: true, }, ); + console.log(myOfflineObservation.coordinates); const observation: ObservationFeature = { type: 'Feature', geometry: { diff --git a/front-end/src/app/pages/new-observation/new-observation.component.ts b/front-end/src/app/pages/new-observation/new-observation.component.ts index 9b5992b..9e00145 100644 --- a/front-end/src/app/pages/new-observation/new-observation.component.ts +++ b/front-end/src/app/pages/new-observation/new-observation.component.ts @@ -298,7 +298,7 @@ export class NewObservationComponent { source: undefined, coordinates: [ this.mapForm.value.position!.lng, - this.mapForm.value.position!.lng, + this.mapForm.value.position!.lat, ], files: this.photoForm.value.photos!.map((photo) => photo.file), }; diff --git a/front-end/src/app/pages/observation-detail/observation-detail.component.html b/front-end/src/app/pages/observation-detail/observation-detail.component.html index 0ba3c1d..8c50cc9 100644 --- a/front-end/src/app/pages/observation-detail/observation-detail.component.html +++ b/front-end/src/app/pages/observation-detail/observation-detail.component.html @@ -1,12 +1,14 @@ -
- +
+ @for (media of observationData.properties.medias; track media.uuid) { + + }
{{ diff --git a/front-end/src/app/pages/observation-detail/observation-detail.component.scss b/front-end/src/app/pages/observation-detail/observation-detail.component.scss index b693459..4a3d8e0 100644 --- a/front-end/src/app/pages/observation-detail/observation-detail.component.scss +++ b/front-end/src/app/pages/observation-detail/observation-detail.component.scss @@ -1,11 +1,14 @@ -.photo-container { +.photos-container { display: flex; - justify-content: center; + overflow-x: scroll; width: 100%; height: 400px; img { width: 100%; object-fit: contain; + &:not(:last-of-type) { + margin-right: 16px; + } } } .source-container { diff --git a/front-end/src/app/pages/synthesis-interface/synthesis-interface.component.scss b/front-end/src/app/pages/synthesis-interface/synthesis-interface.component.scss index f6b54b5..b6bd9e9 100644 --- a/front-end/src/app/pages/synthesis-interface/synthesis-interface.component.scss +++ b/front-end/src/app/pages/synthesis-interface/synthesis-interface.component.scss @@ -5,7 +5,7 @@ $panel-height: 100vh; display: flex; justify-content: center; position: absolute; - bottom: calc(16px + 48px + 56px); + bottom: calc(16px + 48px); z-index: 400; pointer-events: none; button { diff --git a/front-end/src/app/pages/synthesis-interface/synthesis-interface.component.ts b/front-end/src/app/pages/synthesis-interface/synthesis-interface.component.ts index 80b2935..38f4fec 100644 --- a/front-end/src/app/pages/synthesis-interface/synthesis-interface.component.ts +++ b/front-end/src/app/pages/synthesis-interface/synthesis-interface.component.ts @@ -24,6 +24,7 @@ import { observationsFeatureCollection } from '../../types/types'; import { ObservationsService } from '../../services/observations.service'; import { SettingsService } from '../../services/settings.service'; import { environment } from '../../../environments/environment'; +import { firstValueFrom } from 'rxjs'; @Component({ selector: 'app-synthesis-interface', @@ -248,7 +249,7 @@ export class SynthesisInterfaceComponent { data: this.filter, }); - deleteDialogRef.afterClosed().subscribe((result) => { + deleteDialogRef.afterClosed().subscribe(async (result) => { if ( result && result.filter && @@ -257,63 +258,27 @@ export class SynthesisInterfaceComponent { (result.filter.observationDates.start && result.filter.observationDates.end)) ) { - let observationFeatures = null; - - if ( - Boolean(result.filter.observationTypes) && - result.filter.observationTypes.length > 0 - ) { - this.filter.observationTypes = result.filter.observationTypes; - observationFeatures = - this.observationsFeatureCollection?.features.filter( - (feature: any) => - result.filter.observationTypes - .map((observationType: any) => observationType.id) - .includes(feature.properties.id_event_type), - ); - } - if ( - Boolean( - result.filter.observationDates && - result.filter.observationDates.start && - result.filter.observationDates.end, - ) - ) { - this.filter.observationTypes = result.filter.observationTypes; - if (observationFeatures) { - observationFeatures = observationFeatures.filter( - (observationFeature: any) => - moment(observationFeature.properties.date_event).isBetween( - result.filter.observationDates.start, - result.filter.observationDates.end, - null, - '[]', - ), - ); - } else { - observationFeatures = - this.observationsFeatureCollection?.features.filter( - (observationFeature: any) => - moment(observationFeature.properties.date_event).isBetween( - result.filter.observationDates.start, - result.filter.observationDates.end, - null, - '[]', - ), - ); - } - } - this.observationsFeatureCollectionFiltered = { - ...this.observationsFeatureCollection!, - features: observationFeatures || [], - }; - } else { - this.filter = { - observationTypes: [], - observationDates: { start: null, end: null }, - }; + const observations = await firstValueFrom( + this.observationsService.getObservations( + result.filter.observationTypes + ? result.filter.observationTypes.map( + (observationType: any) => observationType.id, + ) + : undefined, + result.filter.observationDates.start + ? moment(result.filter.observationDates.start.toDate()).format( + 'YYYY-MM-DD', + ) + : undefined, + result.filter.observationDates.end + ? moment(result.filter.observationDates.end.toDate()).format( + 'YYYY-MM-DD', + ) + : undefined, + ), + ); this.observationsFeatureCollectionFiltered = - this.observationsFeatureCollection; + observations as observationsFeatureCollection; } if (result && !result.cancel) { this.updateMap(); @@ -343,6 +308,7 @@ export class SynthesisInterfaceComponent { this.bounds && this.map.fitBounds(this.bounds); } + this.handleObservationsWithinBounds(); } handleObservationsWithinBounds() { diff --git a/front-end/src/app/services/auth.service.ts b/front-end/src/app/services/auth.service.ts index de15d07..699d1cc 100644 --- a/front-end/src/app/services/auth.service.ts +++ b/front-end/src/app/services/auth.service.ts @@ -7,7 +7,10 @@ import { User } from '../types/types'; import { OfflineService } from './offline.service'; const httpOptions = { - headers: new HttpHeaders({ 'Content-Type': 'application/json' }), + headers: new HttpHeaders({ + 'Content-Type': 'application/json', + 'Accept-Language': 'fr-FR', + }), }; @Injectable({ diff --git a/front-end/src/app/services/observations.service.ts b/front-end/src/app/services/observations.service.ts index c822162..cb66d81 100644 --- a/front-end/src/app/services/observations.service.ts +++ b/front-end/src/app/services/observations.service.ts @@ -4,11 +4,10 @@ import { environment } from '../../environments/environment'; import { ObservationFeature } from '../types/types'; const httpOptions = { - headers: new HttpHeaders({ 'Content-Type': 'application/json' }), -}; - -const httpMediaOptions = { - headers: new HttpHeaders({ 'Content-Type': 'multipart/form-data' }), + headers: new HttpHeaders({ + 'Content-Type': 'application/json', + 'Accept-Language': 'fr-FR', + }), }; @Injectable({ @@ -19,11 +18,26 @@ export class ObservationsService { constructor() {} - getObservations() { - return this.httpClient.get( - `${environment.apiUrl}/api/observations/`, - httpOptions, - ); + getObservations( + observationTypesId?: number[], + startDate?: string, + endDate?: string, + ) { + let url = `${environment.apiUrl}/api/observations/`; + if (observationTypesId) { + for (let index = 0; index < observationTypesId.length; index++) { + const observationTypeId = observationTypesId[index]; + url = url.concat( + `${index === 0 ? '?' : '&'}categories=${observationTypeId}`, + ); + } + } + if (startDate && endDate) { + url = url.concat( + `${observationTypesId ? '&' : '?'}event_date_after=${startDate}&event_date_before=${endDate}`, + ); + } + return this.httpClient.get(`${url}`, httpOptions); } getObservation(observationId: string) { @@ -49,7 +63,6 @@ export class ObservationsService { } sendPhotoObservation(observationId: any, file: any) { - console.log('sendPhotoObservation', observationId, file); const formData = new FormData(); formData.append('media_file', file); formData.append('media_type', 'image'); diff --git a/front-end/src/app/services/settings.service.ts b/front-end/src/app/services/settings.service.ts index 5aea279..d6655e6 100644 --- a/front-end/src/app/services/settings.service.ts +++ b/front-end/src/app/services/settings.service.ts @@ -6,7 +6,10 @@ import { Settings } from '../types/types'; import { OfflineService } from './offline.service'; const httpOptions = { - headers: new HttpHeaders({ 'Content-Type': 'application/json' }), + headers: new HttpHeaders({ + 'Content-Type': 'application/json', + 'Accept-Language': 'fr-FR', + }), }; const httpIconOptions = { From 27facebede733670f3394ad3a3ef547418284e94 Mon Sep 17 00:00:00 2001 From: Bastyen Date: Thu, 18 Jul 2024 10:49:21 +0200 Subject: [PATCH 08/12] add learn more and legal notice pages --- front-end/src/app/app.component.ts | 25 +++++---- front-end/src/app/app.config.ts | 21 ++++++++ front-end/src/app/app.routes.ts | 22 ++++++++ .../src/app/pages/home/home.component.html | 12 +++++ .../src/app/pages/home/home.component.scss | 5 +- .../learn-more/learn-more.component.html | 0 .../learn-more/learn-more.component.scss | 0 .../learn-more/learn-more.component.spec.ts | 23 ++++++++ .../pages/learn-more/learn-more.component.ts | 12 +++++ .../legal-notice/legal-notice.component.html | 0 .../legal-notice/legal-notice.component.scss | 0 .../legal-notice.component.spec.ts | 23 ++++++++ .../legal-notice/legal-notice.component.ts | 12 +++++ .../new-observation.component.ts | 2 +- .../src/app/services/settings.service.ts | 53 +++++++++++-------- 15 files changed, 177 insertions(+), 33 deletions(-) create mode 100644 front-end/src/app/pages/learn-more/learn-more.component.html create mode 100644 front-end/src/app/pages/learn-more/learn-more.component.scss create mode 100644 front-end/src/app/pages/learn-more/learn-more.component.spec.ts create mode 100644 front-end/src/app/pages/learn-more/learn-more.component.ts create mode 100644 front-end/src/app/pages/legal-notice/legal-notice.component.html create mode 100644 front-end/src/app/pages/legal-notice/legal-notice.component.scss create mode 100644 front-end/src/app/pages/legal-notice/legal-notice.component.spec.ts create mode 100644 front-end/src/app/pages/legal-notice/legal-notice.component.ts diff --git a/front-end/src/app/app.component.ts b/front-end/src/app/app.component.ts index 5d40b0f..d774e3b 100644 --- a/front-end/src/app/app.component.ts +++ b/front-end/src/app/app.component.ts @@ -119,6 +119,22 @@ export class AppComponent { }, { id: 7, + text: 'En savoir plus', + routerLink: 'en-savoir-plus', + authenficated: null, + click: () => null, + observationsPending: false, + }, + { + id: 8, + text: 'Mentions légales', + routerLink: 'legal-notice', + authenficated: null, + click: () => null, + observationsPending: false, + }, + { + id: 0, text: 'Me déconnecter', routerLink: null, authenficated: true, @@ -169,15 +185,6 @@ export class AppComponent { this.handleAuthentification(value); }); - this.settingsService.getSettings().subscribe({ - next: async (settings: any) => { - await this.settingsService.setSettings(settings); - }, - error: async () => { - await this.settingsService.useOfflineSettings(); - }, - }); - this.router.events.subscribe((event) => { if (event instanceof ActivationEnd) { if (this.sidenav.opened) { diff --git a/front-end/src/app/app.config.ts b/front-end/src/app/app.config.ts index 72b9526..259927f 100644 --- a/front-end/src/app/app.config.ts +++ b/front-end/src/app/app.config.ts @@ -2,6 +2,7 @@ import { ApplicationConfig, provideZoneChangeDetection, isDevMode, + APP_INITIALIZER, } from '@angular/core'; import { provideRouter, withComponentInputBinding } from '@angular/router'; @@ -17,6 +18,7 @@ import { } from '@angular/common/http'; import { provideServiceWorker } from '@angular/service-worker'; import { httpInterceptorProviders } from './interceptors/http.interceptor'; +import { SettingsService } from './services/settings.service'; export function tokenGetter() { return localStorage.getItem('access_token'); @@ -45,5 +47,24 @@ export const appConfig: ApplicationConfig = { enabled: !isDevMode(), registrationStrategy: 'registerWhenStable:30000', }), + { + provide: APP_INITIALIZER, + useFactory: (settingsService: SettingsService) => async () => { + return new Promise(async (resolve) => { + settingsService.getSettings().subscribe({ + next: async (settings: any) => { + await settingsService.setSettings(settings); + resolve(true); + }, + error: async () => { + await settingsService.useOfflineSettings(); + resolve(true); + }, + }); + }); + }, + deps: [SettingsService], + multi: true, + }, ], }; diff --git a/front-end/src/app/app.routes.ts b/front-end/src/app/app.routes.ts index 8fb97b2..a6e787e 100644 --- a/front-end/src/app/app.routes.ts +++ b/front-end/src/app/app.routes.ts @@ -11,6 +11,8 @@ import { ForgetPasswordComponent } from './pages/forget-password/forget-password import { ChangePasswordComponent } from './pages/change-password/change-password.component'; import { authGuard } from './guards/auth.guard'; import { ObservationDetailComponent } from './pages/observation-detail/observation-detail.component'; +import { LearnMoreComponent } from './pages/learn-more/learn-more.component'; +import { LegalNoticeComponent } from './pages/legal-notice/legal-notice.component'; export const routes: Routes = [ { @@ -117,5 +119,25 @@ export const routes: Routes = [ }, canActivate: [authGuard], }, + { + path: 'en-savoir-plus', + title: 'En savoir plus', + component: LearnMoreComponent, + data: { + title: 'En savoir plus', + backButton: true, + accountButton: false, + }, + }, + { + path: 'legal-notice', + title: 'Mentions légales', + component: LegalNoticeComponent, + data: { + title: 'Mentions légales', + backButton: true, + accountButton: false, + }, + }, { path: '**', redirectTo: '' }, ]; diff --git a/front-end/src/app/pages/home/home.component.html b/front-end/src/app/pages/home/home.component.html index fcb563d..e4231cb 100644 --- a/front-end/src/app/pages/home/home.component.html +++ b/front-end/src/app/pages/home/home.component.html @@ -1,3 +1,15 @@ +
+
+ Regard d'Altitude est un projet ayant pour but la création d'un réseau + d'échanges entre acteurs de la montagne, des territoires et de la science + autour de la question des transformations de la montagne dans un contexte de + changement climatique. Notamment en permettant un inventaire collaboratif + des évènements en montagne. +
+ +