48 Commits

Author SHA1 Message Date
Rampeur 25d47bc3be Linting 2024-05-09 02:21:19 +02:00
Rampeur 276da97003 Mise à jour angular 2024-05-09 00:06:04 +02:00
Rampeur d1870adff6 Refactoring 2024-05-08 23:46:08 +02:00
Rampeur 7c3f0c9648 Implémentation SkydiverId API 2024-05-08 20:32:49 +02:00
Rampeur 6dece821a8 ---|qcm| Mise à jour V17 et QCM 2024-05-04 02:08:04 +02:00
Rampeur fa93c0c8f2 ---|qcm| Ajout d'une commande de démarrage 2024-03-06 17:38:14 +01:00
Rampeur 249654c419 ---|qcm| Mise à jour de la DT-48 2024-03-06 17:05:29 +01:00
Rampeur 55c1ed0389 ---|qcm| Mise à jour de paquets 2024-02-28 04:29:06 +01:00
Rampeur 8dde054dca ---|qcm| Mise à jour d'angular material et cdk 2024-02-28 04:25:54 +01:00
Rampeur c54267d2a3 ---|qcm| Mise à jour de la version d'angular 2024-02-28 04:23:54 +01:00
Rampeur 5b5f72dcb7 ---|qcm| Mise à jour de la version de typescript 2024-02-28 04:18:05 +01:00
Julien Gautier 85d36d8f9c ---|qcm| Ajout de fichiers de données JSON pour les QCM 2024-01-11 16:58:17 +01:00
Julien Gautier 3ff93184ab ---|qcm| Ajout d'un fichier d'export mongo des utilisateurs 2024-01-11 16:57:04 +01:00
Julien Gautier 1102cc30df ---|qcm| Ajout d'un fichier json d'import des sauts 608 à 649 2024-01-11 16:56:07 +01:00
Julien Gautier 7cd762559a ---|qcm| Mise à jour du model 'QCM' 2024-01-11 16:39:23 +01:00
Julien Gautier 08273863b7 ---|qcm| Mise à jour du component 'QCM' 2023-10-06 01:54:33 +02:00
Julien Gautier 6f2bdf2b4f ---|qcm| Mise à jour du fichier d'import des sauts 2023-10-06 00:53:25 +02:00
Julien Gautier 5c4d52d62f ---|qcm| Ajout du component 'QCM' 2023-10-06 00:51:45 +02:00
Julien Gautier a4f7b3e913 ---|qcm| Mise à jour de liens de menu 2023-10-06 00:51:09 +02:00
Julien Gautier a7b32d4313 ---|qcm| Ajout du model et du service 'QCM' 2023-10-06 00:50:37 +02:00
Julien Gautier 262e7e879b ---|qcm| Ajout du service 'QCM' 2023-10-06 00:49:28 +02:00
Julien Gautier 9e2df18a6e ---|qcm| Ajout du model 'QCM' 2023-10-06 00:49:08 +02:00
Julien Gautier 1a9aeb03c4 Mise à jour d'une feuille de style scss 2023-10-04 19:19:18 +02:00
Julien Gautier 3978128840 Ajout d'une fenetre 'dialog' pour l'affichage du detail d'un saut 2023-10-04 19:18:51 +02:00
Julien Gautier 684fd78352 Update des packages npm 2023-10-04 14:56:20 +02:00
Julien Gautier 296a69c294 Mise à jour d'une feuille de style scss 2023-10-04 14:55:52 +02:00
Julien Gautier f73cd3d81b Ajout d'une commande de démarrage 2023-10-04 14:19:29 +02:00
Julien Gautier 010d957656 Refactoring 2023-10-04 14:18:45 +02:00
Julien Gautier 8d595900b8 Mise à jour du composant 'jump-table' 2023-10-04 14:18:01 +02:00
Julien Gautier 37cbba6cad Ajout du composant 'jump' (détail d'un saut) 2023-10-04 14:16:29 +02:00
Julien Gautier b0c9dbd647 Ajout et mise à jour de configuration d'environnement (local et development) 2023-10-04 14:15:19 +02:00
Julien Gautier 2ccc9fc858 Renommage du composant 'Home' en 'Dashboard' 2023-10-03 16:28:03 +02:00
Julien Gautier 340228e698 Suppression de l'input 'refresh' et refactoring 2023-10-03 14:55:24 +02:00
Julien Gautier 4a9f650483 Ajout du fichier '.eslintrc.json' 2023-10-03 13:50:25 +02:00
Julien Gautier b99f07c184 Mise à jour de feuilles de style 2023-10-03 13:49:55 +02:00
Julien Gautier e591149b7d Mise à jour de 'services' 2023-10-03 13:49:23 +02:00
Julien Gautier 7e3dd53479 Mise à jour de 'models' 2023-10-03 13:48:55 +02:00
Julien Gautier 3a5e837842 Mise à jour de composants 2023-10-03 13:48:26 +02:00
Julien Gautier 6d3e79aac5 Mise à jour de composant partagés 2023-10-03 13:47:48 +02:00
Julien Gautier d0039dc275 Mise à jour de feuilles de style 2023-09-18 22:55:08 +02:00
Julien Gautier efb0485bd9 Mise à jour de 'services' 2023-09-18 22:53:46 +02:00
Julien Gautier 287091a704 Mise à jour de 'models' 2023-09-18 22:53:28 +02:00
Julien Gautier 78518e1653 Mise à jour et ajout de 'components' 2023-09-18 22:52:41 +02:00
Julien Gautier 08dca58ea9 Ajout de chart.js et ng2-charts 2023-09-18 22:51:31 +02:00
Julien Gautier 4988d698da Bump to version 1.0.0 2023-09-14 18:29:10 +02:00
Julien Gautier 92b204c769 Mise à jours du dashboard 2023-09-14 18:28:02 +02:00
Julien Gautier 0e6b3ef1c7 Ajout de components et models 2023-09-13 20:49:23 +02:00
Julien Gautier 1147d5f5bb Mise à jours et ajout de fonctionnalités 2023-09-13 20:48:18 +02:00
302 changed files with 38319 additions and 19464 deletions
+46
View File
@@ -0,0 +1,46 @@
{
"root": true,
"ignorePatterns": [
"projects/**/*"
],
"overrides": [{
"files": [
"*.ts"
],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:@angular-eslint/recommended",
"plugin:@angular-eslint/template/process-inline-templates"
],
"rules": {
"@angular-eslint/directive-selector": [
"error",
{
"type": "attribute",
"prefix": "app",
"style": "camelCase"
}
],
"@angular-eslint/component-selector": [
"error",
{
"type": "element",
"prefix": "app",
"style": "kebab-case"
}
]
}
},
{
"files": [
"*.html"
],
"extends": [
"plugin:@angular-eslint/template/recommended",
"plugin:@angular-eslint/template/accessibility"
],
"rules": {}
}
]
}
+13
View File
@@ -30,6 +30,7 @@ yarn-error.log
# Miscellaneous # Miscellaneous
/.angular/cache /.angular/cache
.nx/
.sass-cache/ .sass-cache/
/connect.lock /connect.lock
/coverage /coverage
@@ -37,6 +38,18 @@ yarn-error.log
testem.log testem.log
/typings /typings
# e2e
/e2e/*.js
/e2e/*.map
# System files # System files
.DS_Store .DS_Store
Thumbs.db Thumbs.db
# SonarQube
.scannerwork/
sonar.properties
**/*.copy.ts
**/*.copy.scss
**/*.copy.html
+2 -2
View File
@@ -1,6 +1,6 @@
# HeadupApp # HeadupAngular
This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 16.1.3. This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 17.2.2.
## Development server ## Development server
+72 -15
View File
@@ -2,44 +2,66 @@
"$schema": "./node_modules/@angular/cli/lib/config/schema.json", "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1, "version": 1,
"cli": { "cli": {
"packageManager": "npm" "packageManager": "npm",
"analytics": false,
"schematicCollections": [
"@angular-eslint/schematics"
]
}, },
"newProjectRoot": "projects", "newProjectRoot": "projects",
"projects": { "projects": {
"headup_app": { "headup_angular": {
"projectType": "application", "projectType": "application",
"schematics": { "schematics": {
"@schematics/angular:component": { "@schematics/angular:component": {
"style": "scss" "style": "scss"
} }
}, },
"i18n": {
"sourceLocale": "fr"
},
"root": "", "root": "",
"sourceRoot": "src", "sourceRoot": "src",
"prefix": "huapp", "prefix": "app",
"architect": { "architect": {
"build": { "build": {
"builder": "@angular-devkit/build-angular:browser", "builder": "@angular-devkit/build-angular:browser-esbuild",
"options": { "options": {
"outputPath": "dist/headup_app", "outputPath": "dist/headup_angular",
"index": "src/index.html", "index": "src/index.html",
"main": "src/main.ts", "main": "src/main.ts",
"tsConfig": "tsconfig.app.json",
"polyfills": [ "polyfills": [
"zone.js" "zone.js"
], ],
"tsConfig": "tsconfig.app.json",
"inlineStyleLanguage": "scss", "inlineStyleLanguage": "scss",
"assets": [ "assets": [
"src/favicon.ico", "src/favicon.ico",
"src/assets", "src/assets",
"src/manifest.webmanifest" "src/manifest.webmanifest",
{
"glob": "_redirects",
"input": "src",
"output": "/"
}
], ],
"styles": [ "styles": [
"node_modules/chartist/dist/index.scss",
"src/styles/styles.scss" "src/styles/styles.scss"
], ],
"stylePreprocessorOptions": {
"includePaths": ["src/styles"]
},
"scripts": [] "scripts": []
}, },
"configurations": { "configurations": {
"production": { "production": {
"buildOptimizer": true,
"extractLicenses": true,
"namedChunks": false,
"optimization": true,
"outputHashing": "all",
"sourceMap": false,
"budgets": [ "budgets": [
{ {
"type": "initial", "type": "initial",
@@ -51,22 +73,45 @@
"maximumWarning": "2kb", "maximumWarning": "2kb",
"maximumError": "4kb" "maximumError": "4kb"
} }
], ]
"outputHashing": "all"
}, },
"development": { "development": {
"buildOptimizer": false, "buildOptimizer": false,
"optimization": false,
"vendorChunk": true,
"extractLicenses": false, "extractLicenses": false,
"sourceMap": true,
"namedChunks": true, "namedChunks": true,
"optimization": false,
"sourceMap": true,
"budgets": [
{
"type": "anyComponentStyle",
"maximumWarning": "6kb"
}
],
"fileReplacements": [ "fileReplacements": [
{ {
"replace": "src/environments/environment.ts", "replace": "src/environments/environment.ts",
"with": "src/environments/environment.development.ts" "with": "src/environments/environment.development.ts"
} }
] ]
},
"local": {
"buildOptimizer": false,
"extractLicenses": false,
"namedChunks": true,
"optimization": false,
"sourceMap": true,
"budgets": [
{
"type": "anyComponentStyle",
"maximumWarning": "6kb"
}
],
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.local.ts"
}
]
} }
}, },
"defaultConfiguration": "production" "defaultConfiguration": "production"
@@ -75,10 +120,13 @@
"builder": "@angular-devkit/build-angular:dev-server", "builder": "@angular-devkit/build-angular:dev-server",
"configurations": { "configurations": {
"production": { "production": {
"browserTarget": "headup_app:build:production" "buildTarget": "headup_angular:build:production"
}, },
"development": { "development": {
"browserTarget": "headup_app:build:development" "buildTarget": "headup_angular:build:development"
},
"local": {
"buildTarget": "headup_angular:build:local"
} }
}, },
"defaultConfiguration": "development" "defaultConfiguration": "development"
@@ -86,7 +134,7 @@
"extract-i18n": { "extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n", "builder": "@angular-devkit/build-angular:extract-i18n",
"options": { "options": {
"browserTarget": "headup_app:build" "buildTarget": "headup_angular:build"
} }
}, },
"test": { "test": {
@@ -108,6 +156,15 @@
], ],
"scripts": [] "scripts": []
} }
},
"lint": {
"builder": "@angular-eslint/builder:lint",
"options": {
"lintFilePatterns": [
"src/**/*.ts",
"src/**/*.html"
]
}
} }
} }
} }
+5925 -13408
View File
File diff suppressed because it is too large Load Diff
+43 -21
View File
@@ -1,44 +1,66 @@
{ {
"name": "headup-app", "name": "headup-app",
"version": "0.0.0", "version": "1.0.0",
"author": "Solide Apps <admin@unespace.com>",
"contributors": [
"Julien Gautier <julien.gautier@unespace.com>"
],
"scripts": { "scripts": {
"ng": "ng", "ng": "ng",
"start": "ng serve", "start": "ng serve",
"build": "ng build", "build": "ng build",
"watch": "ng build --watch --configuration development", "watch": "ng build --watch --configuration development",
"dev": "ng serve --configuration local",
"local": "APP_ENV=local ng serve --port 4400 --configuration local",
"lint": "ng lint",
"test": "ng test" "test": "ng test"
}, },
"engines": {
"node": "^18.19.1 || ^20.11.1"
},
"private": true, "private": true,
"dependencies": { "dependencies": {
"@angular/animations": "^16.1.0", "@angular/animations": "^17.3.5",
"@angular/cdk": "^16.2.1", "@angular/cdk": "^17.3.5",
"@angular/common": "^16.1.0", "@angular/common": "^17.3.5",
"@angular/compiler": "^16.1.0", "@angular/compiler": "^17.3.5",
"@angular/core": "^16.1.0", "@angular/core": "^17.3.5",
"@angular/forms": "^16.1.0", "@angular/forms": "^17.3.8",
"@angular/material": "^16.2.1", "@angular/google-maps": "^17.3.8",
"@angular/platform-browser": "^16.1.0", "@angular/localize": "^17.3.8",
"@angular/platform-browser-dynamic": "^16.1.0", "@angular/material": "^17.3.8",
"@angular/router": "^16.1.0", "@angular/platform-browser": "^17.3.5",
"bootstrap": "^5.3.0", "@angular/platform-browser-dynamic": "^17.3.8",
"@angular/router": "^17.3.8",
"bootstrap": "^5.3.3",
"chart.js": "^4.4.2",
"chartist": "^1.3.0", "chartist": "^1.3.0",
"ng-chartist": "^8.1.3", "ng-chartist": "^8.2.0",
"ngx-skeleton-loader": "^8.0.0", "ng2-charts": "^6.0.1",
"ngx-skeleton-loader": "^9.0.0",
"rxjs": "~7.8.0", "rxjs": "~7.8.0",
"tslib": "^2.3.0", "tslib": "^2.3.0",
"zone.js": "~0.13.0" "zone.js": "^0.14.5"
}, },
"devDependencies": { "devDependencies": {
"@angular-devkit/build-angular": "^16.1.3", "@angular-devkit/build-angular": "^17.3.7",
"@angular/cli": "~16.1.3", "@angular-eslint/builder": "17.3.0",
"@angular/compiler-cli": "^16.1.0", "@angular-eslint/eslint-plugin": "17.3.0",
"@types/jasmine": "~4.3.0", "@angular-eslint/eslint-plugin-template": "17.3.0",
"jasmine-core": "~4.6.0", "@angular-eslint/schematics": "17.3.0",
"@angular-eslint/template-parser": "17.3.0",
"@angular/cli": "^17.3.7",
"@angular/compiler-cli": "^17.3.5",
"@types/jasmine": "~5.1.0",
"@typescript-eslint/eslint-plugin": "7.2.0",
"@typescript-eslint/parser": "7.2.0",
"eslint": "^8.57.0",
"jasmine-core": "~5.1.0",
"karma": "~6.4.0", "karma": "~6.4.0",
"karma-chrome-launcher": "~3.2.0", "karma-chrome-launcher": "~3.2.0",
"karma-coverage": "~2.2.0", "karma-coverage": "~2.2.0",
"karma-jasmine": "~5.1.0", "karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~2.1.0", "karma-jasmine-html-reporter": "~2.1.0",
"typescript": "~5.1.3" "typescript": "~5.3.2"
} }
} }
+1
View File
@@ -0,0 +1 @@
/* /index.html 200
-40
View File
@@ -1,40 +0,0 @@
import { NgModule } from '@angular/core';
import { Routes, RouterModule, PreloadAllModules } from '@angular/router';
const routes: Routes = [
{
path: 'dashboard',
loadChildren: () => import('./components/home/home.module').then(m => m.HomeModule)
},
{
path: 'calculator',
loadChildren: () => import('./components/calculator/calculator.module').then(m => m.CalculatorModule)
},
{
path: 'logbook',
loadChildren: () => import('./components/logbook/logbook.module').then(m => m.LogbookModule)
},
{
path: 'profile',
loadChildren: () => import('./components/profile/profile.module').then(m => m.ProfileModule)
},
{
path: 'settings',
loadChildren: () => import('./components/settings/settings.module').then(m => m.SettingsModule)
}
];
/* @NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
}) */
@NgModule({
imports: [RouterModule.forRoot(routes, {
// preload all modules; optionally we could
// implement a custom preloading strategy for just some
// of the modules (PRs welcome 😉)
preloadingStrategy: PreloadAllModules
})],
exports: [RouterModule]
})
export class AppRoutingModule { }
+1 -9
View File
@@ -1,9 +1 @@
<!-- * * * * * * * * * * * * * * * * * * * * * * * * * * * * * --> <app-full-layout></app-full-layout>
<!-- * * * * * * * * * * Main wrapper * * * * * * * * * * -->
<!-- * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -->
<huapp-full-layout></huapp-full-layout>
<!--
<huapp-layout-header></huapp-layout-header>
<router-outlet><huapp-spinner></huapp-spinner></router-outlet>
<huapp-layout-footer></huapp-layout-footer>
-->
+8 -8
View File
@@ -1,12 +1,12 @@
import { TestBed } from '@angular/core/testing'; import { TestBed } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { AppComponent } from './app.component'; import { AppComponent } from './app.component';
describe('AppComponent', () => { describe('AppComponent', () => {
beforeEach(() => TestBed.configureTestingModule({ beforeEach(async () => {
imports: [RouterTestingModule], await TestBed.configureTestingModule({
declarations: [AppComponent] imports: [AppComponent],
})); }).compileComponents();
});
it('should create the app', () => { it('should create the app', () => {
const fixture = TestBed.createComponent(AppComponent); const fixture = TestBed.createComponent(AppComponent);
@@ -14,16 +14,16 @@ describe('AppComponent', () => {
expect(app).toBeTruthy(); expect(app).toBeTruthy();
}); });
it(`should have as title 'Head Up'`, () => { it(`should have the 'headup_angular' title`, () => {
const fixture = TestBed.createComponent(AppComponent); const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance; const app = fixture.componentInstance;
expect(app.title).toEqual('Head Up'); expect(app.title).toEqual('headup_angular');
}); });
it('should render title', () => { it('should render title', () => {
const fixture = TestBed.createComponent(AppComponent); const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges(); fixture.detectChanges();
const compiled = fixture.nativeElement as HTMLElement; const compiled = fixture.nativeElement as HTMLElement;
expect(compiled.querySelector('.content span')?.textContent).toContain('Head Up app is running!'); expect(compiled.querySelector('h1')?.textContent).toContain('Hello, headup_angular');
}); });
}); });
+28 -5
View File
@@ -1,15 +1,38 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { MAT_DATE_LOCALE, MAT_DATE_FORMATS, provideNativeDateAdapter } from '@angular/material/core';
import { Title } from '@angular/platform-browser'; import { Title } from '@angular/platform-browser';
import { FullComponent } from 'src/app/components/shared/layout';
import { UserService } from "src/app/core/services"; import { UserService } from "src/app/core/services";
import { SpinnerComponent } from 'src/app/components';
export const MY_FORMATS = {
parse: {
dateInput: 'DD/MM/YYYY',
},
display: {
dateInput: 'DD/MM/YYYY',
monthYearLabel: 'MMM YYYY',
dateA11yLabel: 'LL',
monthYearA11yLabel: 'MMMM-YYYY',
},
};
@Component({ @Component({
selector: 'huapp-root', selector: 'app-root',
standalone: true,
imports: [
FullComponent
],
providers: [
{ provide: MAT_DATE_LOCALE, useValue: 'fr-FR' },
{ provide: MAT_DATE_FORMATS, useValue: MY_FORMATS },
provideNativeDateAdapter(MY_FORMATS)
],
templateUrl: './app.component.html', templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'] styleUrl: './app.component.scss'
}) })
export class AppComponent { export class AppComponent implements OnInit {
title = 'Head Up'; public title = 'Head Up';
constructor(private userService: UserService, private titleService: Title) { } constructor(private userService: UserService, private titleService: Title) { }
+40
View File
@@ -0,0 +1,40 @@
import { APP_INITIALIZER, ApplicationConfig, LOCALE_ID } from '@angular/core';
import { Title } from '@angular/platform-browser';
//import { provideRouter, withComponentInputBinding, withDebugTracing } from '@angular/router';
import { provideRouter, withViewTransitions } from '@angular/router';
import { provideAnimations } from '@angular/platform-browser/animations';
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { registerLocaleData } from '@angular/common';
import localeFr from '@angular/common/locales/fr';
import { provideCharts, withDefaultRegisterables } from 'ng2-charts';
import { EMPTY } from "rxjs";
registerLocaleData(localeFr);
import { routes } from './app.routes';
import { apiInterceptor, tokenInterceptor, errorInterceptor } from "src/app/core/interceptors";
import { JwtService, UserService } from "src/app/core/services";
export function initAuth(jwtService: JwtService, userService: UserService) {
return () => (jwtService.getToken() ? userService.getCurrentUser() : EMPTY);
}
export const appConfig: ApplicationConfig = {
providers: [
//provideRouter(routes, withComponentInputBinding(), withDebugTracing()),
provideRouter(routes, withViewTransitions()),
provideHttpClient(
withInterceptors([apiInterceptor, tokenInterceptor, errorInterceptor])
),
provideAnimations(),
provideCharts(withDefaultRegisterables()),
Title,
{ provide: LOCALE_ID, useValue: 'fr-FR' },
{
provide: APP_INITIALIZER,
useFactory: initAuth,
deps: [JwtService, UserService],
multi: true,
}
]
};
-30
View File
@@ -1,30 +0,0 @@
import { LOCALE_ID, NgModule } from '@angular/core';
import { BrowserModule, Title } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { registerLocaleData } from '@angular/common';
import localeFr from '@angular/common/locales/fr';
import { ChartistModule } from "ng-chartist";
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { CoreModule } from './core/core.module';
import { AuthModule } from './components/auth/auth.module';
import { HomeModule } from './components/home/home.module';
import { SpinnerComponent, FullComponent } from './components/shared';
registerLocaleData(localeFr);
@NgModule({
declarations: [ AppComponent ],
imports: [
BrowserModule,
AppRoutingModule,
ChartistModule,
BrowserAnimationsModule,
AuthModule, CoreModule, HomeModule,
SpinnerComponent, FullComponent
],
providers: [ Title, { provide: LOCALE_ID, useValue: 'fr-FR' } ],
bootstrap: [ AppComponent ]
})
export class AppModule { }
+5
View File
@@ -0,0 +1,5 @@
import { Routes } from '@angular/router';
import { AUTH_ROUTES, NOAUTH_ROUTES } from 'src/app/routes';
export const routes: Routes = [ ...AUTH_ROUTES, ...NOAUTH_ROUTES];
@@ -0,0 +1,49 @@
<div @flyInOut>
<div class="d-flex">
<div class="flex-fill">
<h1 class="mb-1">{{title}}</h1>
<span [hidden]="!lastJump.numero" class="me-2 fs-6">
Dernier saut enregistré : <span class="fs-5 mx-1 text-primary">{{ lastJump.numero }}<sup>ème</sup></span> à <span class="fs-5 mx-1 text-primary">{{ lastJump.lieu }}</span> le <span class="fs-5 mx-1 text-primary">{{ lastJump.date | date: 'dd/MM/yyyy' }}</span>
</span>
</div>
<span class="flex-spacer"></span>
<div class="align-self-center me-2">
<button mat-icon-button [matMenuTriggerFor]="menuAeronef" aria-label="Menu Aeronef">
<mat-icon fontIcon="more_vert"></mat-icon>
</button>
<mat-menu #menuAeronef="matMenu">
@for (menuitem of menuItems.getMenuPanel(); track menuitem) {
<button mat-menu-item>
<mat-icon [fontIcon]="menuitem.icon"></mat-icon>
<span>{{ menuitem.name }}</span>
</button>
}
</mat-menu>
</div>
</div>
<mat-divider class="my-3"></mat-divider>
<mat-card>
<mat-card-header>
<mat-card-title>{{ subtitle }}</mat-card-title>
</mat-card-header>
<mat-card-content class="mt-4">
@if (displayCharts) {
<div class="row">
<div class="col-xl-4 col-lg-5 col-md-6 col-sm-12">
<app-pie-chart [headers]="seriesHeader" [names]="seriesName" [values]="seriesValue" [colors]="seriesColor"></app-pie-chart>
</div>
<div class="col-xl-8 col-lg-7 col-md-6 col-sm-12">
<!--<app-bar-chart [headers]="seriesHeader" [names]="seriesName" [values]="seriesValue" [colors]="seriesColor" label="Nombre total de sauts" [legend]="false"></app-bar-chart>-->
<app-bars-chart [headers]="seriesHeader" [names]="seriesName" [values]="seriesRow" [colors]="seriesColor" [legend]="false"></app-bars-chart>
</div>
</div>
}
<div class="d-flex">
<div class="flex-fill py-2">
<mat-divider class="mt-3 mb-0"></mat-divider>
<app-history-table [headers]="seriesHeader" [names]="seriesName" [values]="seriesValue" [rows]="seriesRow" [colors]="seriesColorClass"></app-history-table>
</div>
</div>
</mat-card-content>
</mat-card>
</div>
@@ -1,4 +1,4 @@
import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing'; import { ComponentFixture, TestBed } from '@angular/core/testing';
import { AeronefsComponent } from './aeronefs.component'; import { AeronefsComponent } from './aeronefs.component';
@@ -6,14 +6,12 @@ describe('AeronefsComponent', () => {
let component: AeronefsComponent; let component: AeronefsComponent;
let fixture: ComponentFixture<AeronefsComponent>; let fixture: ComponentFixture<AeronefsComponent>;
beforeEach(waitForAsync(() => { beforeEach(async () => {
TestBed.configureTestingModule({ await TestBed.configureTestingModule({
declarations: [ AeronefsComponent ] imports: [AeronefsComponent]
}) })
.compileComponents(); .compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(AeronefsComponent); fixture = TestBed.createComponent(AeronefsComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
fixture.detectChanges(); fixture.detectChanges();
@@ -0,0 +1,113 @@
import { DatePipe } from '@angular/common';
import { trigger, state, style, animate, transition } from '@angular/animations';
import { Component, DestroyRef, inject, OnInit, OnDestroy } from '@angular/core';
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { ActivatedRoute } from '@angular/router';
import { MatBadgeModule } from '@angular/material/badge';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatDividerModule } from '@angular/material/divider';
import { MatExpansionModule } from '@angular/material/expansion';
import { MatGridListModule } from '@angular/material/grid-list';
import { MatIconModule } from '@angular/material/icon';
import { MatMenuModule } from '@angular/material/menu';
import { Observable, Subscription } from 'rxjs';
import { MenuItems } from 'src/app/components/shared';
import { HistoryTableComponent } from 'src/app/components/shared/helpers-jump';
import { BarsChartComponent, PieChartComponent } from 'src/app/components/shared/helpers-chart';
import { Jump, AeronefsPageData } from 'src/app/core/models';
import { UtilitiesService } from 'src/app/core/services';
import { AeronefByImat, AeronefByYear } from 'src/app/core/models';
@Component({
selector: 'app-aeronefs',
standalone: true,
imports: [
DatePipe,
MatBadgeModule, MatButtonModule, MatCardModule, MatDividerModule,
MatExpansionModule, MatGridListModule, MatIconModule, MatMenuModule,
BarsChartComponent, HistoryTableComponent, PieChartComponent
],
templateUrl: './aeronefs.component.html',
styleUrl: './aeronefs.component.scss',
animations: [
trigger('flyInOut', [
state('in', style({ transform: 'translateX(0)' })),
transition('void => *', [
style({ transform: 'translateX(-100%)' }),
animate(200)
]),
transition('* => void', [
style({ transform: 'translateX(100%)' }),
animate(200)
])
])
]
})
export class AeronefsComponent implements OnInit, OnDestroy {
private _data: Subscription = new Subscription();
private _aeronefsByImat!: Array<AeronefByImat>;
private _aeronefsByYear!: Array<AeronefByYear>;
public lastJump: Jump = {} as Jump;
public title = 'Aéronefs';
public subtitle: string = 'Nombre total de sauts par aéronef';
public displayCharts = false;
public destroyRef = inject(DestroyRef);
public seriesHeader: string[] = [];
public seriesName: string[] = [];
public seriesValue: number[] = [];
public seriesRow: Array<Array<number>> = [];
public seriesColor: {
backgroundColor: string[],
borderColor: string[]
} = {
backgroundColor: this._utilitiesService.getSeriesColors(0.8, 'all'),
borderColor: this._utilitiesService.getSeriesColors(1, 'all')
};
public seriesColorClass: string[] = this._utilitiesService.getChartColors();
constructor(
private route: ActivatedRoute,
private _utilitiesService: UtilitiesService,
public menuItems: MenuItems
) { }
ngOnInit() {
const data$: Observable<{ aeronefsPageData: AeronefsPageData }> = this.route.data as Observable<{ aeronefsPageData: AeronefsPageData }>;
this._data = data$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((data: { aeronefsPageData: AeronefsPageData }) => {
const pageData: AeronefsPageData = data.aeronefsPageData;
this.lastJump = pageData.lastjump;
this._loadAeronefByImat(pageData);
});
}
ngOnDestroy() {
this._data.unsubscribe();
}
private _loadAeronefByImat(pageData: AeronefsPageData): void {
this._aeronefsByImat = pageData.aeronefsByImat.map((row: AeronefByImat) => {
this.seriesName.push(`${row.aeronef} ${row.imat}`);
this.seriesValue.push(row.count);
return row;
});
this._loadAeronefByYear(pageData);
}
private _loadAeronefByYear(pageData: AeronefsPageData): void {
this.seriesHeader = ['2018', '2019', '2020', '2021', '2022', '2023', '2024'];
const values: Array<number> = this.seriesHeader.map(() => 0);
this.seriesName.forEach(() => {
this.seriesRow.push([...values]);
});
this._aeronefsByYear = pageData.aeronefsByImatByYear.map((row: AeronefByYear) => {
const aeronef: string = row.aeronef;
const imat: string = row.imat;
const year: string = row.year.toString();
this.seriesRow[this.seriesName.indexOf(`${aeronef} ${imat}`)][this.seriesHeader.indexOf(year)] = row.count;
return row;
});
this.displayCharts = true;
}
}
@@ -1,29 +0,0 @@
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { NoAuthGuard } from 'src/app/core/services/no-auth-guard.service';
import { AuthComponent } from './auth.component';
const routes: Routes = [
{
path: 'login',
component: AuthComponent,
canActivate: [NoAuthGuard]
},
{
path: 'register',
component: AuthComponent,
canActivate: [NoAuthGuard]
},
{
path: 'authenticate',
component: AuthComponent,
canActivate: [NoAuthGuard]
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class AuthRoutingModule { }
+60 -18
View File
@@ -1,66 +1,108 @@
<div class="container page auth-page"> <div class="container auth-page">
<div class="row"> <div class="row">
<div class="col-md-6 offset-md-3 col-xs-12"> <div class="col-md-6 offset-md-3 col-xs-12">
<h1 class="text-xs-center">{{ title }}</h1> <h1 class="text-xs-center">{{ title }}</h1>
<p class="text-xs-center"> <p class="text-xs-center">
<a [routerLink]="['/login']" *ngIf="authType === 'register'">Vous avez déjà un compte ?</a> @if (authType === 'register') {
<a [routerLink]="['/register']" *ngIf="authType === 'login'">Vous n'avez pas encore de compte ?</a> <a [routerLink]="['/login']">Vous avez déjà un compte ?</a>
}
@if (authType === 'login') {
<a [routerLink]="['/register']">Vous n'avez pas encore de compte ?</a>
}
</p> </p>
<huapp-list-errors [errors]="errors"></huapp-list-errors> <app-list-errors [errors]="errors"></app-list-errors>
<form [formGroup]="authForm" (ngSubmit)="submitForm()"> <form [formGroup]="authForm" (ngSubmit)="submitForm()">
<fieldset [disabled]="isSubmitting" class="w-100"> <fieldset [disabled]="isSubmitting" class="w-100">
<legend [hidden]="true">Informations personnelles</legend> <legend [hidden]="true">Informations personnelles</legend>
<div class="col-12"> <div class="col-12">
<mat-form-field appearance="fill" class="w-100"> <mat-form-field appearance="fill" class="w-100">
<input matInput type="text" placeholder="Votre adresse email" formControlName="email" required> <input matInput type="text" placeholder="Votre adresse email" formControlName="email" required>
<mat-error *ngIf="this.authForm.controls['email'].status === 'INVALID'">{{getErrorMessage('email')}}</mat-error> @if (this.authForm.controls['email'].status === 'INVALID') {
<mat-error>{{getErrorMessage('email')}}</mat-error>
}
</mat-form-field> </mat-form-field>
</div> </div>
<div class="col-12" *ngIf="authType === 'register'"> @if (authType === 'register') {
<div class="col-12">
<mat-form-field appearance="fill" class="w-100"> <mat-form-field appearance="fill" class="w-100">
<input matInput type="text" placeholder="Votre nom" formControlName="lastname" required> <input matInput type="text" placeholder="Votre nom" formControlName="lastname" required>
<mat-error *ngIf="this.authForm.controls['lastname'].status === 'INVALID'">{{getErrorMessage('lastname')}}</mat-error> @if (this.authForm.controls['lastname'].status === 'INVALID') {
<mat-error>{{getErrorMessage('lastname')}}</mat-error>
}
</mat-form-field> </mat-form-field>
</div> </div>
<div class="col-12" *ngIf="authType === 'register'"> }
@if (authType === 'register') {
<div class="col-12">
<mat-form-field appearance="fill" class="w-100"> <mat-form-field appearance="fill" class="w-100">
<input matInput type="text" placeholder="Votre prénom" formControlName="firstname" required> <input matInput type="text" placeholder="Votre prénom" formControlName="firstname" required>
<mat-error *ngIf="this.authForm.controls['firstname'].status === 'INVALID'">{{getErrorMessage('firstname')}}</mat-error> @if (this.authForm.controls['firstname'].status === 'INVALID') {
<mat-error>{{getErrorMessage('firstname')}}</mat-error>
}
</mat-form-field> </mat-form-field>
</div> </div>
<div class="col-12" *ngIf="authType === 'register'"> }
@if (authType === 'register') {
<div class="col-12">
<mat-form-field appearance="fill" class="w-100"> <mat-form-field appearance="fill" class="w-100">
<input matInput type="text" placeholder="Numéro de téléphone" formControlName="phone" required> <input matInput type="text" placeholder="Numéro de téléphone" formControlName="phone" required>
<mat-error *ngIf="this.authForm.controls['phone'].status === 'INVALID'">{{getErrorMessage('phone')}}</mat-error> @if (this.authForm.controls['phone'].status === 'INVALID') {
<mat-error>{{getErrorMessage('phone')}}</mat-error>
}
</mat-form-field> </mat-form-field>
</div> </div>
<div class="col-12" *ngIf="authType === 'register'"> }
@if (authType === 'register') {
<div class="col-12">
<mat-form-field appearance="fill" class="w-100"> <mat-form-field appearance="fill" class="w-100">
<input matInput type="text" placeholder="Pseudo" formControlName="username"> <input matInput type="text" placeholder="Pseudo" formControlName="username">
<mat-error *ngIf="this.authForm.controls['username'].status === 'INVALID'">{{getErrorMessage('username')}}</mat-error> @if (this.authForm.controls['username'].status === 'INVALID') {
<mat-error>{{getErrorMessage('username')}}</mat-error>
}
</mat-form-field> </mat-form-field>
</div> </div>
<div class="col-12" *ngIf="authType === 'register'"> }
@if (authType === 'register') {
<div class="col-12">
<mat-form-field appearance="fill" class="w-100"> <mat-form-field appearance="fill" class="w-100">
<input matInput type="text" placeholder="Numéro de licence FFP" formControlName="licence"> <input matInput type="text" placeholder="Numéro de licence FFP" formControlName="licence">
<mat-error *ngIf="this.authForm.controls['licence'].status === 'INVALID'">{{getErrorMessage('licence')}}</mat-error> @if (this.authForm.controls['licence'].status === 'INVALID') {
<mat-error>{{getErrorMessage('licence')}}</mat-error>
}
</mat-form-field> </mat-form-field>
</div> </div>
}
@if (authType === 'register') {
<div class="col-12">
<mat-form-field appearance="fill" class="w-100">
<input matInput type="text" placeholder="Indiquez le poids en Kg" formControlName="poids">
@if (this.authForm.controls['poids'].status === 'INVALID') {
<mat-error>{{getErrorMessage('poids')}}</mat-error>
}
</mat-form-field>
</div>
}
</fieldset> </fieldset>
<fieldset [disabled]="isSubmitting" class="w-100"> <fieldset [disabled]="isSubmitting" class="w-100">
<legend [hidden]="true">Informations de connexion</legend> <legend [hidden]="true">Informations de connexion</legend>
<div class="col-12"> <div class="col-12">
<mat-form-field appearance="fill" class="w-100"> <mat-form-field appearance="fill" class="w-100">
<input matInput type="password" placeholder="Mot de passe" formControlName="password" required> <input matInput type="password" placeholder="Mot de passe" formControlName="password" required>
<mat-error *ngIf="this.authForm.controls['password'].status === 'INVALID'">{{getErrorMessage('password')}}</mat-error> @if (this.authForm.controls['password'].status === 'INVALID') {
<mat-error>{{getErrorMessage('password')}}</mat-error>
}
</mat-form-field> </mat-form-field>
</div> </div>
<div class="col-12" *ngIf="authType === 'register'"> @if (authType === 'register') {
<div class="col-12">
<mat-form-field appearance="fill" class="w-100"> <mat-form-field appearance="fill" class="w-100">
<input matInput type="password" placeholder="Confirmez votre nouveau mot de passe" formControlName="confirmPassword" required> <input matInput type="password" placeholder="Confirmez votre nouveau mot de passe" formControlName="confirmPassword" required>
<mat-error *ngIf="this.authForm.controls['confirmPassword'].status === 'INVALID'">{{getErrorMessage('confirmPassword')}}</mat-error> @if (this.authForm.controls['confirmPassword'].status === 'INVALID') {
<mat-error>{{getErrorMessage('confirmPassword')}}</mat-error>
}
</mat-form-field> </mat-form-field>
</div> </div>
}
<div class="col-12"> <div class="col-12">
<button mat-raised-button color="primary" [disabled]="!authForm.valid" type="submit" class="float-sm-end">{{ btnTitle }}</button> <button mat-raised-button color="primary" [disabled]="!authForm.valid" type="submit" class="float-sm-end">{{ btnTitle }}</button>
</div> </div>
@@ -8,8 +8,8 @@ describe('AuthComponent', () => {
beforeEach(() => { beforeEach(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [AuthComponent] imports: [AuthComponent]
}); });
fixture = TestBed.createComponent(AuthComponent); fixture = TestBed.createComponent(AuthComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
fixture.detectChanges(); fixture.detectChanges();
+28 -17
View File
@@ -1,5 +1,5 @@
import { Component, OnInit, OnDestroy } from '@angular/core'; import { Component, DestroyRef, inject, OnInit, OnDestroy } from '@angular/core';
import { NgIf } from '@angular/common'; import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { UntypedFormBuilder, UntypedFormGroup, AbstractControl, Validators, ValidatorFn, ValidationErrors, FormsModule, ReactiveFormsModule } from '@angular/forms'; import { UntypedFormBuilder, UntypedFormGroup, AbstractControl, Validators, ValidatorFn, ValidationErrors, FormsModule, ReactiveFormsModule } from '@angular/forms';
import { Title } from '@angular/platform-browser'; import { Title } from '@angular/platform-browser';
import { ActivatedRoute, Router, RouterLink } from '@angular/router'; import { ActivatedRoute, Router, RouterLink } from '@angular/router';
@@ -7,7 +7,7 @@ import { MatButtonModule } from '@angular/material/button';
import { MatInputModule } from '@angular/material/input'; import { MatInputModule } from '@angular/material/input';
import { MatFormFieldModule } from '@angular/material/form-field'; import { MatFormFieldModule } from '@angular/material/form-field';
import { Observable, Observer, Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
import { take } from 'rxjs/operators'; import { take } from 'rxjs/operators';
import { Errors } from 'src/app/core/models'; import { Errors } from 'src/app/core/models';
@@ -17,13 +17,18 @@ import { ListErrorsComponent, ShowAuthedDirective } from 'src/app/components/sha
@Component({ @Component({
standalone: true, standalone: true,
imports: [ imports: [
NgIf, RouterLink, FormsModule, ReactiveFormsModule, RouterLink,
MatFormFieldModule, MatInputModule, MatButtonModule, FormsModule,
ListErrorsComponent, ShowAuthedDirective ReactiveFormsModule,
MatFormFieldModule,
MatInputModule,
MatButtonModule,
ListErrorsComponent,
ShowAuthedDirective
], ],
selector: 'huapp-auth', selector: 'app-auth',
templateUrl: './auth.component.html', templateUrl: './auth.component.html',
styleUrls: ['./auth.component.scss'] styleUrl: './auth.component.scss'
}) })
export class AuthComponent implements OnInit, OnDestroy { export class AuthComponent implements OnInit, OnDestroy {
private _url: Subscription = new Subscription(); private _url: Subscription = new Subscription();
@@ -31,9 +36,10 @@ export class AuthComponent implements OnInit, OnDestroy {
authType = ''; authType = '';
title = ''; title = '';
btnTitle = ''; btnTitle = '';
errors!: Errors; errors: Errors = { errors: {} };
isSubmitting = false; isSubmitting = false;
authForm!: UntypedFormGroup; authForm!: UntypedFormGroup;
destroyRef = inject(DestroyRef);
constructor( constructor(
private route: ActivatedRoute, private route: ActivatedRoute,
@@ -44,15 +50,18 @@ export class AuthComponent implements OnInit, OnDestroy {
) { ) {
this._resetErrors(); this._resetErrors();
// use FormBuilder to create a form group // use FormBuilder to create a form group
this.authForm = this.fb.group({ const controlsConfig = {
email: ['', [Validators.required, Validators.email]], email: ['', [Validators.required, Validators.email]],
username: '', username: '',
firstname: '', firstname: '',
lastname: '', lastname: '',
phone: '', phone: '',
licence: '',
poids: '',
password: ['', Validators.required], password: ['', Validators.required],
confirmPassword: '' confirmPassword: ''
}, { validators: this.checkPasswords }); };
this.authForm = this.fb.group(controlsConfig, { validators: this.checkPasswords });
} }
ngOnInit() { ngOnInit() {
@@ -75,10 +84,10 @@ export class AuthComponent implements OnInit, OnDestroy {
} }
// add form control for username if this is the register page // add form control for username if this is the register page
if (this.authType === 'register') { if (this.authType === 'register') {
this.authForm.controls["firstname"].setValidators([Validators.required]); this.authForm.controls['firstname'].setValidators([Validators.required]);
this.authForm.controls["lastname"].setValidators([Validators.required]); this.authForm.controls['lastname'].setValidators([Validators.required]);
this.authForm.controls["phone"].setValidators([Validators.required]); this.authForm.controls['phone'].setValidators([Validators.required]);
this.authForm.controls["confirmPassword"].setValidators([Validators.minLength(8), Validators.required]); this.authForm.controls['confirmPassword'].setValidators([Validators.minLength(8), Validators.required]);
} }
}); });
} }
@@ -90,14 +99,16 @@ export class AuthComponent implements OnInit, OnDestroy {
submitForm() { submitForm() {
this.isSubmitting = true; this.isSubmitting = true;
this.errors = { errors: {} };
if (this.authForm.valid) { if (this.authForm.valid) {
this._resetErrors(); this._resetErrors();
const credentials = this.authForm.value; const credentials = this.authForm.value;
const user$ = this.userService.attemptAuth(this.authType, credentials).pipe(take(1)); const user$ = this.userService.attemptAuth(this.authType, credentials).pipe(takeUntilDestroyed(this.destroyRef));
this._user = user$.subscribe({ this._user = user$.subscribe({
next: () => this.router.navigateByUrl('/'), next: () => void this.router.navigateByUrl('/'),
error: (err) => { error: (err) => {
console.log(err);
this.errors = err; this.errors = err;
this.isSubmitting = false; this.isSubmitting = false;
} }
-14
View File
@@ -1,14 +0,0 @@
import { NgModule } from '@angular/core';
import { NoAuthGuard } from 'src/app/core/services/no-auth-guard.service';
import { AuthComponent } from './auth.component';
import { AuthRoutingModule } from './auth-routing.module';
@NgModule({
imports: [
AuthRoutingModule,
AuthComponent
],
providers: [NoAuthGuard]
})
export class AuthModule { }
@@ -1,18 +0,0 @@
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { CalculatorComponent } from './calculator.component';
import { AuthGuard } from 'src/app/core/services';
const routes: Routes = [
{
path: '',
component: CalculatorComponent,
canActivate: [AuthGuard]
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class CalculatorRoutingModule { }
@@ -1,278 +1,257 @@
<div class="content"> <div @flyInOut>
<h1>{{title}}</h1> <div class="d-flex">
<div class="col-xs-12"> <div class="flex-fill">
<div class="demo-buttons my-2"> <h1 class="mb-1">{{title}}</h1>
<!-- primary buttons --> <span class="me-2 fs-6"><span class="fs-5 mx-1">DT48</span> - 08 février 2024</span>
<button mat-button color="primary" class="me-2">Texte</button>
<button mat-stroked-button color="primary" class="me-2">Texte Stroked</button>
<button mat-flat-button color="primary" class="me-2">Texte Flat</button>
<button mat-raised-button color="primary" class="me-2">Texte Raised</button>
<button mat-icon-button color="primary" class="me-2">
<mat-icon>check_circle</mat-icon>
</button>
<button mat-fab color="primary" class="me-2">
<mat-icon>check_circle</mat-icon>
</button>
<button mat-mini-fab color="primary" class="me-2">
<mat-icon>check_circle</mat-icon>
</button>
</div> </div>
<div class="demo-buttons my-2"> <span class="flex-spacer"></span>
<!-- accent buttons --> <div class="align-self-center me-2">
<button mat-button color="accent" class="me-2">Texte</button> <button mat-icon-button [matMenuTriggerFor]="menuCalculator" aria-label="Menu Calculator">
<button mat-stroked-button color="accent" class="me-2">Texte Stroked</button>
<button mat-flat-button color="accent" class="me-2">Texte Flat</button>
<button mat-raised-button color="accent" class="me-2">Texte Raised</button>
<button mat-icon-button color="accent" class="me-2">
<mat-icon>check_circle</mat-icon>
</button>
<button mat-fab color="accent" class="me-2">
<mat-icon>check_circle</mat-icon>
</button>
<button mat-mini-fab color="accent" class="me-2">
<mat-icon>check_circle</mat-icon>
</button>
</div>
<div class="demo-buttons my-2">
<!-- warn buttons -->
<button mat-button color="warn" class="me-2">Texte</button>
<button mat-stroked-button color="warn" class="me-2">Texte Stroked</button>
<button mat-flat-button color="warn" class="me-2">Texte Flat</button>
<button mat-raised-button color="warn" class="me-2">Texte Raised</button>
<button mat-icon-button color="warn" class="me-2">
<mat-icon>check_circle</mat-icon>
</button>
<button mat-fab color="warn" class="me-2">
<mat-icon>check_circle</mat-icon>
</button>
<button mat-mini-fab color="warn" class="me-2">
<mat-icon>check_circle</mat-icon>
</button>
</div>
<div class="demo-buttons my-2">
<!-- danger buttons -->
<button mat-button color="danger" class="me-2">Texte</button>
<button mat-stroked-button color="danger" class="me-2">Texte Stroked</button>
<button mat-flat-button color="danger" class="me-2">Texte Flat</button>
<button mat-raised-button color="danger" class="me-2">Texte Raised</button>
<button mat-icon-button color="danger" class="me-2">
<mat-icon>check_circle</mat-icon>
</button>
<button mat-fab color="danger" class="me-2">
<mat-icon>check_circle</mat-icon>
</button>
<button mat-mini-fab color="danger" class="me-2">
<mat-icon>check_circle</mat-icon>
</button>
</div>
<div class="demo-buttons my-2">
<!-- info buttons -->
<button mat-button color="info" class="me-2">Texte</button>
<button mat-stroked-button color="info" class="me-2">Texte Stroked</button>
<button mat-flat-button color="info" class="me-2">Texte Flat</button>
<button mat-raised-button color="info" class="me-2">Texte Raised</button>
<button mat-icon-button color="info" class="me-2">
<mat-icon>info</mat-icon>
</button>
<button mat-fab color="info" class="me-2">
<mat-icon>info</mat-icon>
</button>
<button mat-mini-fab color="info" class="me-2">
<mat-icon>info</mat-icon>
</button>
</div>
<div class="demo-buttons my-2">
<!-- success buttons -->
<button mat-button color="success" class="me-2">Texte</button>
<button mat-stroked-button color="success" class="me-2">Texte Stroked</button>
<button mat-flat-button color="success" class="me-2">Texte Flat</button>
<button mat-raised-button color="success" class="me-2">Texte Raised</button>
<button mat-icon-button color="success" class="me-2">
<mat-icon>check_circle</mat-icon>
</button>
<button mat-fab color="success" class="me-2">
<mat-icon>check_circle</mat-icon>
</button>
<button mat-mini-fab color="success" class="me-2">
<mat-icon>check_circle</mat-icon>
</button>
</div>
</div>
<section>
<div class="demo-label">Basic</div>
<div class="demo-button-row">
<button mat-button>Basic</button>
<button mat-button color="primary">Primary</button>
<button mat-button color="accent">Accent</button>
<button mat-button color="warn">Warn</button>
<button mat-button color="danger">Danger</button>
<button mat-button color="info">Info</button>
<button mat-button color="success">Success</button>
<button mat-button disabled>Disabled</button>
<a mat-button href="https://www.google.com/" target="_blank">Link</a>
</div>
</section>
<mat-divider></mat-divider>
<section>
<div class="demo-label">Raised</div>
<div class="demo-button-row">
<button mat-raised-button>Basic</button>
<button mat-raised-button color="primary">Primary</button>
<button mat-raised-button color="accent">Accent</button>
<button mat-raised-button color="warn">Warn</button>
<button mat-raised-button color="danger">Danger</button>
<button mat-raised-button color="info">Info</button>
<button mat-raised-button color="success">Success</button>
<button mat-raised-button disabled>Disabled</button>
<a mat-raised-button href="https://www.google.com/" target="_blank">Link</a>
</div>
</section>
<mat-divider></mat-divider>
<section>
<div class="demo-label">Stroked</div>
<div class="demo-button-row">
<button mat-stroked-button>Basic</button>
<button mat-stroked-button color="primary">Primary</button>
<button mat-stroked-button color="accent">Accent</button>
<button mat-stroked-button color="warn">Warn</button>
<button mat-stroked-button color="danger">Danger</button>
<button mat-stroked-button color="info">Info</button>
<button mat-stroked-button color="success">Success</button>
<button mat-stroked-button disabled>Disabled</button>
<a mat-stroked-button href="https://www.google.com/" target="_blank">Link</a>
</div>
</section>
<mat-divider></mat-divider>
<section>
<div class="demo-label">Flat</div>
<div class="demo-button-row">
<button mat-flat-button>Basic</button>
<button mat-flat-button color="primary">Primary</button>
<button mat-flat-button color="accent">Accent</button>
<button mat-flat-button color="warn">Warn</button>
<button mat-flat-button color="danger">Danger</button>
<button mat-flat-button color="info">Info</button>
<button mat-flat-button color="success">Success</button>
<button mat-flat-button disabled>Disabled</button>
<a mat-flat-button href="https://www.google.com/" target="_blank">Link</a>
</div>
</section>
<mat-divider></mat-divider>
<section>
<div class="demo-label">Icon</div>
<div class="demo-button-row">
<div class="demo-flex-container">
<button mat-icon-button aria-label="Example icon button with a vertical three dot icon">
<mat-icon>more_vert</mat-icon> <mat-icon>more_vert</mat-icon>
</button> </button>
<button mat-icon-button color="primary" aria-label="Example icon button with a home icon"> <mat-menu #menuCalculator="matMenu">
<mat-icon>home</mat-icon> @for (menuitem of menuItems.getMenuCalculator(); track menuitem) {
</button> @if (menuitem.type === 'link') {
<button mat-icon-button color="accent" aria-label="Example icon button with a menu icon"> <button mat-menu-item [routerLink]="['/', menuitem.state]">
<mat-icon>menu</mat-icon> <mat-icon [fontIcon]="menuitem.icon"></mat-icon>
</button> <span>{{ menuitem.name }}</span>
<button mat-icon-button color="warn" aria-label="Example icon button with a heart icon">
<mat-icon>favorite</mat-icon>
</button>
<button mat-icon-button color="danger" aria-label="Example icon button with a heart icon">
<mat-icon>favorite</mat-icon>
</button>
<button mat-icon-button color="info" aria-label="Example icon button with a heart icon">
<mat-icon>favorite</mat-icon>
</button>
<button mat-icon-button color="success" aria-label="Example icon button with a heart icon">
<mat-icon>favorite</mat-icon>
</button>
<button mat-icon-button disabled aria-label="Example icon button with a open in new tab icon">
<mat-icon>open_in_new</mat-icon>
</button> </button>
}
}
</mat-menu>
</div> </div>
</div> </div>
</section> <mat-divider class="my-3"></mat-divider>
<mat-divider></mat-divider> <mat-card class="calculator">
<section> <mat-card-header class="bg-warn rounded-top pt-1 pb-2 px-2">
<div class="demo-label">FAB</div> <form #searchFormData="ngForm" class="inputs-form">
<div class="demo-button-row"> <mat-form-field class="bg-crystal me-3" subscriptSizing="dynamic" color="accent">
<div class="demo-flex-container"> <mat-label>Nombre de sauts</mat-label>
<div class="demo-button-container"> <input matInput [(ngModel)]="inputs.jumps" (change)="refresh()" type="number" min="0" max="2000"
<button mat-fab color="primary" aria-label="Example icon button with a delete icon"> step="1" name="jumps">
<mat-icon>delete</mat-icon> </mat-form-field>
</button> <mat-form-field class="bg-crystal me-3" subscriptSizing="dynamic" color="accent">
<mat-label>Poids nu</mat-label>
<input matInput [(ngModel)]="inputs.weight" (change)="refresh()" type="number" min="30"
max="110" name="weight">
</mat-form-field>
<mat-form-field class="bg-crystal me-3" subscriptSizing="dynamic" color="accent">
<mat-label>Poids équipement</mat-label>
<input matInput [(ngModel)]="inputs.gear" (change)="refresh()" type="number" min="0"
name="gear">
</mat-form-field>
<mat-form-field class="bg-crystal me-3" subscriptSizing="dynamic" color="accent">
<mat-label>Taille actuelle</mat-label>
<input matInput [(ngModel)]="inputs.current" (change)="refresh()" type="number" min="0"
name="current" id="current" placeholder="Taille actuelle">
</mat-form-field>
</form>
<span class="flex-spacer"></span>
<div class="flex-fill"></div>
</mat-card-header>
<mat-card-content class="my-4">
<div class="row">
<div class="col-lg-6 col-md-8 col-sm-12">
<table class="table table-striped table-dark table-hover mat-elevation-z2">
<thead>
<tr class="warn">
<th class="text-nowrap pe-4"></th>
<th class="text-nowrap">Taille actuelle</th>
<th class="text-nowrap">Taille min</th>
<th class="text-nowrap">Taille min -11%</th>
</tr>
</thead>
<tbody>
<tr class="warn">
<th class="text-nowrap pe-4">Taille de voile en ft<sup>2</sup></th>
<td class="font-monospace text-{{ info.state.color }}">
{{ info.sizesFeet.current }} </td>
<td class="font-monospace text-success"> ● {{ info.sizesFeet.min }} </td>
<td class="font-monospace text-warning"> ● {{ info.sizesFeet.min11 }} </td>
</tr>
<tr class="warn">
<th class="text-nowrap pe-4">Charge alaire</th>
<td class="font-monospace">{{ info.charges.current | number : '1.2-3' }}</td>
<td class="font-monospace">{{ info.charges.min | number : '1.2-3' }}</td>
<td class="font-monospace">{{ info.charges.min11 | number : '1.2-3' }}</td>
</tr>
<tr class="warn">
<th class="text-nowrap pe-4">Taille de voile en m<sup>2</sup></th>
<td class="font-monospace">{{ info.sizesMeter.current | number : '1.2-3' }}</td>
<td class="font-monospace">{{ info.sizesMeter.min | number : '1.2-3' }}</td>
<td class="font-monospace">{{ info.sizesMeter.min11 | number : '1.2-3' }}</td>
</tr>
<tr class="warn">
<th class="text-nowrap pe-4">Coefficient kilo/livres</th>
<td colspan="3" class="font-monospace text-start">{{ coeffKgLbs }}</td>
</tr>
<tr class="warn">
<th class="text-nowrap pe-4">Coefficient pied/mètre</th>
<td colspan="3" class="font-monospace text-start">{{ coeffFtM }}</td>
</tr>
<tr class="warn">
<th class="text-nowrap pe-4">Formule charge alaire</th>
<td colspan="3" class="font-monospace text-start">
( ( {{ inputs.weight }} + {{ inputs.gear }} ) * {{ coeffKgLbs }} ) /
{{ inputs.current }} = {{ info.charges.current | number : '1.5' }}<br />
( ( poids nu en kg + poids equipement en kg ) * coefficient kg/lbs ) / taille de
voile en ft<sup>2</sup>
</td>
</tr>
</tbody>
</table>
</div> </div>
<div class="demo-button-container"> <div class="col-lg-6 col-md-8 col-sm-12">
<button mat-fab color="accent" aria-label="Example icon button with a bookmark icon"> @if (displayCharts) {
<mat-icon>bookmark</mat-icon> <div class="linechart position-relative w-100 mt-3">
</button> <canvas baseChart class="mx-auto" width="800" height="280"
[data]="chartConfig.lineChartData" [options]="chartConfig.lineChartOptions"
[legend]="chartConfig.lineChartLegend" [type]="'line'">
</canvas>
</div> </div>
<div class="demo-button-container"> }
<button mat-fab color="warn" aria-label="Example icon button with a home icon">
<mat-icon>home</mat-icon>
</button>
</div>
<div class="demo-button-container">
<button mat-fab color="danger" aria-label="Example icon button with a heart icon">
<mat-icon>favorite</mat-icon>
</button>
</div>
<div class="demo-button-container">
<button mat-fab color="info" aria-label="Example icon button with a heart icon">
<mat-icon>favorite</mat-icon>
</button>
</div>
<div class="demo-button-container">
<button mat-fab color="success" aria-label="Example icon button with a heart icon">
<mat-icon>favorite</mat-icon>
</button>
</div>
<div class="demo-button-container">
<button mat-fab disabled aria-label="Example icon button with a heart icon">
<mat-icon>favorite</mat-icon>
</button>
</div> </div>
</div> </div>
<mat-divider class="my-3"></mat-divider>
<h4 class="py-2 mb-0">Tailles de voile pour {{ inputs.weight }} kg</h4>
<div>
Prochain changement de valeurs dans {{ ((this.currentRange.end + 1) - inputs.jumps) }}
{{ ((this.currentRange.end + 1) - inputs.jumps) > 1 ? 'sauts' : 'saut'}}<br />
</div> </div>
</section> <div class="text-end">
<mat-divider></mat-divider> {{ ((((inputs.jumps - this.currentRange.start) + 1) / ((this.currentRange.end - this.currentRange.start) + 1)) * 100) | number : '1.2-2' }}%
<section> de la tranche actuelle ({{this.currentRange.start}} à {{this.currentRange.end}})
<div class="demo-label">Mini FAB</div>
<div class="demo-button-row">
<div class="demo-flex-container">
<div class="demo-button-container">
<button mat-mini-fab color="primary" aria-label="Example icon button with a menu icon">
<mat-icon>menu</mat-icon>
</button>
</div> </div>
<div class="demo-button-container"> <mat-progress-bar mode="determinate"
<button mat-mini-fab color="accent" aria-label="Example icon button with a plus one icon"> [value]="((((inputs.jumps - this.currentRange.start) + 1) / ((this.currentRange.end - this.currentRange.start) + 1)) * 100)"
<mat-icon>plus_one</mat-icon> color="warn"></mat-progress-bar>
</button> <table class="table table-striped table-dark table-hover mat-elevation-z2 mb-1">
<thead>
<tr class="warn">
<th class="text-nowrap pe-4"></th>
@for (header of sizesHeader; track header; let index = $index) {
<th class="pl-3 pr-3 {{ tableHeader[(index+1)].active }}">{{ header.name }}</th>
}
</tr>
</thead>
<tbody>
<tr class="warn">
<th class="firtext-nowrap pe-4st">Taille de voile en ft<sup>2</sup></th>
@for (value of sizesValuesFeet; track value; let index = $index) {
<td class="pl-3 text-nowrap {{ tableHeader[(index+1)].active }}">{{ value }}</td>
}
</tr>
<tr class="warn">
<th class="text-nowrap pe-4">Abbattement 11%</th>
@for (value of sizesMinValuesFeet; track value; let index = $index) {
<td class="pl-3 text-nowrap {{ tableHeader[(index+1)].active }}">{{ value }}</td>
}
</tr>
<tr class="warn">
<th class="text-nowrap pe-4">Charge alaire</th>
@for (charge of sizesCharges; track charge; let index = $index) {
<td class="pl-3 text-nowrap {{ tableHeader[(index+1)].active }}">
{{ charge | number : '1.2-3' }}</td>
}
</tr>
<tr class="warn">
<th class="text-nowrap pe-4">Taille de voile en m<sup>2</sup></th>
@for (value of sizesValuesMeter; track value; let index = $index) {
<td class="pl-3 text-nowrap {{ tableHeader[(index+1)].active }}">
{{ value | number : '1.2-2' }}</td>
}
</tr>
</tbody>
</table>
<mat-divider class="my-3"></mat-divider>
<h4 class="py-2 mb-0">Tableau des tailles de voile par poids</h4>
<div class="text-end">
{{ ((inputs.jumps / 1600) * 100) | number : '1.2-2' }}% de la plage complète (0 à 1600)
</div> </div>
<div class="demo-button-container"> <mat-progress-bar mode="determinate" [value]="((inputs.jumps / 1600) * 100)" color="warn">
<button mat-mini-fab color="warn" aria-label="Example icon button with a filter list icon"> </mat-progress-bar>
<mat-icon>filter_list</mat-icon> <table mat-table [dataSource]="canopy_size_table" matSort
</button> class="table table-striped table-dark table-hover mat-elevation-z2">
<!-- weight column -->
<ng-container matColumnDef="weight">
<th mat-header-cell *matHeaderCellDef mat-sort-header class="text-nowrap pe-4 {{tableHeader[0].active}}">
{{tableHeader[0].name}} </th>
<th mat-cell *matCellDef="let element;" class="text-end ps-3 pe-4 fs-6" role="button"
(click)="setCurrentWeight(element.weight)">
<span class="text-warn d-block">{{element.weight}}</span>
</th>
</ng-container>
<!-- range_1 column -->
<ng-container matColumnDef="range_1">
<th class="pl-3" mat-header-cell *matHeaderCellDef class="{{tableHeader[1].active}}">
{{tableHeader[1].name}} </th>
<td mat-cell *matCellDef="let element;"
class="pl-3 text-nowrap fs-6 {{element.ranges[0].active}}">{{element.ranges[0].value}}</td>
</ng-container>
<!-- range_2 column -->
<ng-container matColumnDef="range_2">
<th class="pl-3" mat-header-cell *matHeaderCellDef class="{{tableHeader[2].active}}">
{{tableHeader[2].name}} </th>
<td mat-cell *matCellDef="let element;"
class="pl-3 text-nowrap fs-6 {{element.ranges[1].active}}">{{element.ranges[1].value}}</td>
</ng-container>
<!-- range_3 column -->
<ng-container matColumnDef="range_3">
<th class="pl-3" mat-header-cell *matHeaderCellDef class="{{tableHeader[3].active}}">
{{tableHeader[3].name}} </th>
<td mat-cell *matCellDef="let element;"
class="pl-3 text-nowrap fs-6 {{element.ranges[2].active}}">{{element.ranges[2].value}}</td>
</ng-container>
<!-- range_4 column -->
<ng-container matColumnDef="range_4">
<th class="pl-3" mat-header-cell *matHeaderCellDef class="{{tableHeader[4].active}}">
{{tableHeader[4].name}} </th>
<td mat-cell *matCellDef="let element;"
class="pl-3 text-nowrap fs-6 {{element.ranges[3].active}}">{{element.ranges[3].value}}</td>
</ng-container>
<!-- range_5 column -->
<ng-container matColumnDef="range_5">
<th class="pl-3" mat-header-cell *matHeaderCellDef class="{{tableHeader[5].active}}">
{{tableHeader[5].name}} </th>
<td mat-cell *matCellDef="let element;"
class="pl-3 text-nowrap fs-6 {{element.ranges[4].active}}">{{element.ranges[4].value}}</td>
</ng-container>
<!-- range_6 column -->
<ng-container matColumnDef="range_6">
<th class="pl-3" mat-header-cell *matHeaderCellDef class="{{tableHeader[6].active}}">
{{tableHeader[6].name}} </th>
<td mat-cell *matCellDef="let element;"
class="pl-3 text-nowrap fs-6 {{element.ranges[5].active}}">{{element.ranges[5].value}}</td>
</ng-container>
<!-- range_7 column -->
<ng-container matColumnDef="range_7">
<th class="pl-3" mat-header-cell *matHeaderCellDef class="{{tableHeader[7].active}}">
{{tableHeader[7].name}} </th>
<td mat-cell *matCellDef="let element;"
class="pl-3 text-nowrap fs-6 {{element.ranges[6].active}}">{{element.ranges[6].value}}</td>
</ng-container>
<!-- range_8 column -->
<ng-container matColumnDef="range_8">
<th class="pl-3" mat-header-cell *matHeaderCellDef class="{{tableHeader[8].active}}">
{{tableHeader[8].name}} </th>
<td mat-cell *matCellDef="let element;"
class="pl-3 text-nowrap fs-6 {{element.ranges[7].active}}">{{element.ranges[7].value}}</td>
</ng-container>
<!-- range_9 column -->
<ng-container matColumnDef="range_9">
<th class="pl-3" mat-header-cell *matHeaderCellDef class="{{tableHeader[9].active}}">
{{tableHeader[9].name}} </th>
<td mat-cell *matCellDef="let element;"
class="pl-3 text-nowrap fs-6 {{element.ranges[8].active}}">{{element.ranges[8].value}}</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns; sticky: false" class="warn"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"
class="text-middle font-monospace warn {{row.active}}" id="weight_{{row.weight}}"></tr>
</table>
<div class="clearfix">
<mat-paginator [pageSize]="50" [pageSizeOptions]="[5, 10, 15, 25, 50]" showFirstLastButtons
class="pull-right mb-1 mt-2"></mat-paginator>
</div> </div>
<div class="demo-button-container"> </mat-card-content>
<button mat-mini-fab color="danger" aria-label="Example icon button with a heart icon"> </mat-card>
<mat-icon>favorite</mat-icon>
</button>
</div>
<div class="demo-button-container">
<button mat-mini-fab color="info" aria-label="Example icon button with a heart icon">
<mat-icon>favorite</mat-icon>
</button>
</div>
<div class="demo-button-container">
<button mat-mini-fab color="success" aria-label="Example icon button with a heart icon">
<mat-icon>favorite</mat-icon>
</button>
</div>
<div class="demo-button-container">
<button mat-mini-fab disabled aria-label="Example icon button with a home icon">
<mat-icon>home</mat-icon>
</button>
</div>
</div>
</div>
</section>
</div> </div>
@@ -1,26 +1,33 @@
section { .calculator-page {
section {
display: table; display: table;
} }
.demo-label { .demo-label {
display: table-cell; display: table-cell;
font-size: 14px; font-size: 14px;
margin-left: 8px; margin-left: 8px;
min-width: 120px; min-width: 120px;
} }
.demo-button-row { .demo-button-row {
display: table-cell; display: table-cell;
max-width: 900px; max-width: 900px;
} }
.demo-button-row .mat-mdc-button-base { .demo-button-row .mat-mdc-button-base {
margin: 8px 8px 8px 0; margin: 8px 8px 8px 0;
} }
.demo-flex-container { .demo-flex-container {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
flex-wrap: wrap; flex-wrap: wrap;
} }
.demo-button-container { .demo-button-container {
display: flex; display: flex;
justify-content: center; justify-content: center;
width: 120px; width: 120px;
}
th {
&.first {
width: 180px;
}
}
} }
@@ -8,8 +8,8 @@ describe('CalculatorComponent', () => {
beforeEach(() => { beforeEach(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [CalculatorComponent] imports: [CalculatorComponent]
}); });
fixture = TestBed.createComponent(CalculatorComponent); fixture = TestBed.createComponent(CalculatorComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
fixture.detectChanges(); fixture.detectChanges();
@@ -1,28 +1,222 @@
import { Component, OnInit, OnDestroy } from '@angular/core'; import { Component, Input, OnInit, OnDestroy, ViewChild } from '@angular/core';
import { trigger, state, style, animate, transition } from '@angular/animations';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { RouterModule } from '@angular/router';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatDividerModule } from '@angular/material/divider'; import { MatDividerModule } from '@angular/material/divider';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon'; import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { MatMenuModule } from '@angular/material/menu';
import { MatPaginator, MatPaginatorModule } from '@angular/material/paginator';
import { MatProgressBarModule } from '@angular/material/progress-bar';
import { MatSort, MatSortModule } from '@angular/material/sort';
import { MatTableDataSource, MatTableModule } from '@angular/material/table';
import { BaseChartDirective } from 'ng2-charts';
import { Observable, Subscription } from 'rxjs';
import { MenuItems } from 'src/app/components/shared';
import { CalculatorInfo, CalculatorResult, CalculatorState, InputParams, Jump, LineConfig, WeightSizeRange, TableHeader, User, WeightSize, weightSizes } from 'src/app/core/models';
import { CalculatorService, JumpsService, UserService, UtilitiesService } from 'src/app/core/services';
@Component({ @Component({
selector: 'app-calculator',
standalone: true, standalone: true,
imports: [ imports: [
MatButtonModule, MatDividerModule, MatIconModule CommonModule, FormsModule, RouterModule,
MatButtonModule, MatCardModule, MatDividerModule,
MatFormFieldModule, MatInputModule, MatIconModule, MatMenuModule,
MatPaginatorModule, MatProgressBarModule, MatSortModule, MatTableModule,
BaseChartDirective
], ],
selector: 'huapp-calculator',
templateUrl: './calculator.component.html', templateUrl: './calculator.component.html',
styleUrls: ['./calculator.component.scss'] styleUrl: './calculator.component.scss',
animations: [
trigger('flyInOut', [
state('in', style({ transform: 'translateY(0)' })),
transition('void => *', [
style({ transform: 'translateY(-100%)' }),
animate(200)
]),
transition('* => void', [
style({ transform: 'translateY(100%)' }),
animate(200)
])
])
]
}) })
export class CalculatorComponent implements OnInit, OnDestroy { export class CalculatorComponent implements OnInit, OnDestroy {
title = 'Taille de voile - DT48'; //private _lastjump!: Subscription; // = new Subscription();
//private _currentUser!: Subscription; // = new Subscription();
private _subscriptions: Array<Subscription> = [];
public title = 'Calcul de taille de voile';
public subtitle = 'DT48 - 08 février 2024';
public lastjump: Jump = {} as Jump;
public currentRange: WeightSizeRange = {} as WeightSizeRange;
public currentUser: User = {} as User;
public displayedColumns: Array<string> = ['weight'];
public tableHeader: Array<TableHeader> = [<TableHeader>{name: 'Poids nu en kg', active: ''}];
public sizesHeader: Array<TableHeader> = [];
public sizesValuesFeet: Array<number> = [];
public sizesMinValuesFeet: Array<number> = [];
public sizesCharges: Array<number> = [];
public sizesValuesMeter: Array<number> = [];
public displayCharts = false;
public chartConfig: LineConfig = this._utilitiesService.getLineChartConfig();
public coeffKgLbs: number = this._utilitiesService.getCoeffKgLbs();
public coeffFtM: number = this._utilitiesService.getCoeffFtM();
public info: CalculatorInfo = {
charges: <CalculatorResult>{ current: 0, min: 0, min11: 0 },
sizesFeet: <CalculatorResult>{ current: 0, min: 0, min11: 0 },
sizesMeter: <CalculatorResult>{ current: 0, min: 0, min11: 0 },
state: <CalculatorState>{ color: 'danger' }
}
constructor() { } @Input() canopy_size_table!: MatTableDataSource<WeightSize>;
@Input() inputs: InputParams = <InputParams>{
jumps: 0,
weight: 60,
gear: 10,
current: 190
} as InputParams;
@ViewChild(MatSort) sort!: MatSort;
@ViewChild(MatPaginator) paginator!: MatPaginator;
constructor(
private _calculatorService: CalculatorService,
private _jumpsService: JumpsService,
private _userService: UserService,
private _utilitiesService: UtilitiesService,
public menuItems: MenuItems
) { }
ngOnInit() { ngOnInit() {
// const currentUser$: Observable<User> = this._userService.currentUser;
const lastjump$: Observable<Jump> = this._jumpsService.getLastJump();
weightSizes[0].ranges.forEach((range: WeightSizeRange) => {
const header: TableHeader = {name: range.label, active: range.active};
this.tableHeader.push(header)
this.sizesHeader.push(header);
this.displayedColumns.push(range.name);
});
this.chartConfig.lineChartData.labels = [...this.sizesHeader.map(
(data: TableHeader): string => data.name
)];
this.chartConfig.lineChartData.datasets = [
{
label: 'Tailles min -11%',
data: this.sizesMinValuesFeet,
fill: true,
stepped: true,
pointStyle: false,
borderColor: 'rgba(241, 80, 80, 1)',
backgroundColor: 'rgba(241, 80, 80, 0.3)'
},
{
label: 'Tailles min',
data: this.sizesValuesFeet,
fill: true,
stepped: true,
pointStyle: false,
borderColor: 'rgba(32, 182, 252, 1)',
backgroundColor: 'rgba(32, 182, 252, 0.3)'
}
];
this._subscriptions.push(
currentUser$.subscribe((userData: User) => {
this.currentUser = userData;
this._subscriptions.push(
lastjump$.subscribe((jump) => {
this.lastjump = jump;
this.inputs.jumps = jump.numero;
this.inputs.weight = this.currentUser.poids!;
this.inputs.current = jump.taille!;
this.canopy_size_table = new MatTableDataSource<WeightSize>(weightSizes);
this.refresh();
})
);
})
);
} }
ngOnDestroy() { ngOnDestroy() {
//this._scavenger.unsubscribe(); this._subscriptions.forEach((sub: Subscription) => {
sub.unsubscribe();
});
}
public refresh():void {
this._refreshActive();
this._refreshSizes();
}
private _refreshActive(): void {
let weight: number = this.inputs.weight;
if (this.inputs.weight < 60) {
weight = 60;
}
weightSizes.forEach(element => {
element.active = '';
element.ranges.forEach(range => {
range.active = '';
});
});
this.tableHeader.forEach(element => {
element.active = '';
});
if (weight >= 60 && weight <= 110) {
weightSizes[(weight - 60)].active = 'active';
}
if (this.inputs.jumps >= 0) {
const num = this._calculatorService.getRangeNum(this.inputs.jumps);
this.tableHeader[num].active = 'active';
weightSizes.forEach(element => {
element.ranges[(num-1)].active = 'active';
});
this.currentRange = weightSizes[0].ranges[(num-1)];
}
}
private _refreshSizes(): void {
let weight: number = this.inputs.weight;
if (this.inputs.weight < 60) {
weight = 60;
}
if (this.inputs.jumps > 2000) {
this.info.state.error = '2K+ Jumps';
}
this.sizesValuesFeet = this._calculatorService.getCanopySizes(weight);
this.sizesMinValuesFeet = this._calculatorService.getCanopySizes(weight, true);
this.sizesValuesMeter = this.sizesValuesFeet.map((value: number, index: number) => {
this.sizesCharges[index] = this._calculatorService.getCharge(value, this.inputs.weight, this.inputs.gear);
return this._calculatorService.convertFeet2Meters(value);
});
this.info.sizesFeet = this._calculatorService.canopySizeCalc(weight, this.inputs.jumps);
this.info.sizesFeet.current = this.inputs.current;
this.info.sizesMeter = {
current: this._calculatorService.convertFeet2Meters(this.inputs.current),
min: this._calculatorService.convertFeet2Meters(this.info.sizesFeet.min),
min11: this._calculatorService.convertFeet2Meters(this.info.sizesFeet.min11)
}
this.info.charges = {
current: this._calculatorService.getCharge(this.inputs.current, this.inputs.weight, this.inputs.gear),
min: this._calculatorService.getCharge(this.info.sizesFeet.min, this.inputs.weight, this.inputs.gear),
min11: this._calculatorService.getCharge(this.info.sizesFeet.min11, this.inputs.weight, this.inputs.gear)
}
this.info.state.color = this._calculatorService.getStateColor(this.info.sizesFeet);
this.chartConfig.lineChartData.datasets[0].data = this.sizesMinValuesFeet;
this.chartConfig.lineChartData.datasets[1].data = this.sizesValuesFeet;
this.displayCharts = true;
}
public setCurrentWeight(weight: number) {
this.inputs.weight = weight;
this.refresh();
} }
} }
@@ -1,15 +0,0 @@
import { NgModule } from '@angular/core';
import { AuthResolver } from 'src/app/core/services';
import { CalculatorComponent } from './calculator.component';
import { CalculatorRoutingModule } from './calculator-routing.module';
@NgModule({
imports: [
CalculatorRoutingModule,
CalculatorComponent
],
exports: [],
providers: [AuthResolver]
})
export class CalculatorModule { }
@@ -0,0 +1,56 @@
<div @flyInOut>
<div class="d-flex">
<div class="flex-fill">
<h1 class="mb-1">{{title}}</h1>
<span [hidden]="!lastJump.numero" class="me-2 fs-6">
Dernier saut enregistré : <span class="fs-5 mx-1 text-primary">{{ lastJump.numero }}<sup>ème</sup></span> à <span class="fs-5 mx-1 text-primary">{{ lastJump.lieu }}</span> le <span class="fs-5 mx-1 text-primary">{{ lastJump.date | date: 'dd/MM/yyyy' }}</span>
</span>
</div>
<span class="flex-spacer"></span>
<div class="align-self-center me-2">
<button mat-icon-button [matMenuTriggerFor]="menuCanopyModel" aria-label="Menu Canopy Model">
<mat-icon fontIcon="more_vert"></mat-icon>
</button>
<mat-menu #menuCanopyModel="matMenu">
@for (menuitem of menuItems.getMenuPanel(); track menuitem) {
<button mat-menu-item>
<mat-icon [fontIcon]="menuitem.icon"></mat-icon>
<span>{{ menuitem.name }}</span>
</button>
}
</mat-menu>
</div>
</div>
<mat-divider class="my-3"></mat-divider>
<mat-card>
<mat-card-header>
<mat-card-title>{{ subtitle }}</mat-card-title>
</mat-card-header>
<mat-card-content class="mt-4">
@if (displayCharts) {
<div class="row">
<div class="col-xl-3 col-lg-6 col-md-6 col-sm-12">
<app-pie-chart [headers]="seriesModelHeader" [names]="seriesModelName" [values]="seriesModelValue" [colors]="seriesColor"></app-pie-chart>
</div>
<div class="col-xl-3 col-lg-6 col-md-6 col-sm-12">
<app-pie-chart [headers]="seriesSizeHeader" [names]="seriesSizeName" [values]="seriesSizeValue" [colors]="seriesColor"></app-pie-chart>
</div>
<div class="col-xl-6 col-lg-12 col-md-12">
<!--<app-pie-chart [headers]="seriesSizeHeader" [names]="seriesSizeName" [values]="seriesSizeValue" [colors]="seriesColor"></app-pie-chart>-->
<app-bars-chart [headers]="seriesSizeHeader" [names]="seriesSizeName" [values]="seriesSizeRow" [colors]="seriesColor" [legend]="false"></app-bars-chart>
</div>
</div>
}
<div class="row">
<div class="col-xl-6 col-lg-12">
<mat-divider class="mt-3 mb-0"></mat-divider>
<app-history-table [headers]="seriesModelHeader" [names]="seriesModelName" [values]="seriesModelValue" [rows]="seriesModelRow" [colors]="seriesColorClass"></app-history-table>
</div>
<div class="col-xl-6 col-lg-12">
<mat-divider class="mt-3 mb-0"></mat-divider>
<app-history-table [headers]="seriesSizeHeader" [names]="seriesSizeName" [values]="seriesSizeValue" [rows]="seriesSizeRow" [colors]="seriesColorClass"></app-history-table>
</div>
</div>
</mat-card-content>
</mat-card>
</div>
@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { CanopiesComponent } from './canopies.component';
describe('CanopiesComponent', () => {
let component: CanopiesComponent;
let fixture: ComponentFixture<CanopiesComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [CanopiesComponent]
})
.compileComponents();
fixture = TestBed.createComponent(CanopiesComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,142 @@
import { DatePipe } from '@angular/common';
import { trigger, state, style, animate, transition } from '@angular/animations';
import { Component, DestroyRef, inject, OnInit, OnDestroy } from '@angular/core';
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { ActivatedRoute } from '@angular/router';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatDividerModule } from '@angular/material/divider';
import { MatExpansionModule } from '@angular/material/expansion';
import { MatGridListModule } from '@angular/material/grid-list';
import { MatIconModule } from '@angular/material/icon';
import { MatMenuModule } from '@angular/material/menu';
import { Observable, Subscription } from 'rxjs';
import { MenuItems } from 'src/app/components/shared';
import { HistoryTableComponent } from 'src/app/components/shared/helpers-jump';
import { BarsChartComponent, PieChartComponent } from 'src/app/components/shared/helpers-chart';
import { Jump, CanopiesPageData } from 'src/app/core/models';
import { UtilitiesService } from 'src/app/core/services';
import { CanopyBySize, CanopyByYear, CanopyModelBySize, CanopyModelByYear } from 'src/app/core/models';
@Component({
selector: 'app-canopies',
standalone: true,
imports: [
DatePipe,
MatButtonModule, MatCardModule, MatDividerModule, MatExpansionModule,
MatGridListModule, MatIconModule, MatMenuModule,
HistoryTableComponent, BarsChartComponent, PieChartComponent
],
templateUrl: './canopies.component.html',
styleUrl: './canopies.component.scss',
animations: [
trigger('flyInOut', [
state('in', style({ transform: 'translateX(0)' })),
transition('void => *', [
style({ transform: 'translateX(-100%)' }),
animate(200)
]),
transition('* => void', [
style({ transform: 'translateX(100%)' }),
animate(200)
])
])
]
})
export class CanopiesComponent implements OnInit, OnDestroy {
private _data: Subscription = new Subscription();
private _canopiesModelBySize!: Array<CanopyModelBySize>;
private _canopiesModelByYear!: Array<CanopyModelByYear>;
private _canopiesSizeBySize!: Array<CanopyBySize>;
private _canopiesSizeByYear!: Array<CanopyByYear>;
public lastJump: Jump = {} as Jump;
public title = 'Voiles';
public subtitle: string = 'Nombre total de sauts par modèle et taille';
public displayCharts = false;
public destroyRef = inject(DestroyRef);
public seriesModelHeader: string[] = [];
public seriesModelName: string[] = [];
public seriesModelValue: number[] = [];
public seriesModelRow: Array<Array<number>> = [];
public seriesSizeHeader: string[] = [];
public seriesSizeName: string[] = [];
public seriesSizeValue: number[] = [];
public seriesSizeRow: Array<Array<number>> = [];
public seriesColor: {
backgroundColor: string[],
borderColor: string[]
} = {
backgroundColor: this._utilitiesService.getSeriesColors(0.8, 'all'),
borderColor: this._utilitiesService.getSeriesColors(1, 'all')
};
public seriesColorClass: string[] = this._utilitiesService.getChartColors();
constructor(
private route: ActivatedRoute,
private _utilitiesService: UtilitiesService,
public menuItems: MenuItems
) { }
ngOnInit() {
const data$: Observable<{ canopiesPageData: CanopiesPageData }> = this.route.data as Observable<{ canopiesPageData: CanopiesPageData }>;
this._data = data$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((data: { canopiesPageData: CanopiesPageData }) => {
const pageData: CanopiesPageData = data.canopiesPageData;
this.lastJump = pageData.lastjump;
this._loadCanopyBySize(pageData);
this._loadCanopyModelBySize(pageData);
});
}
ngOnDestroy() {
this._data.unsubscribe();
}
private _loadCanopyBySize(pageData: CanopiesPageData): void {
this._canopiesSizeBySize = pageData.canopiesBySize.map((row: CanopyBySize) => {
this.seriesSizeName.push(row.taille.toString());
this.seriesSizeValue.push(row.count);
return row;
});
this._loadCanopyByYear(pageData);
}
private _loadCanopyByYear(pageData: CanopiesPageData): void {
this.seriesSizeHeader = ['2018', '2019', '2020', '2021', '2022', '2023', '2024'];
const values: Array<number> = this.seriesSizeHeader.map(() => 0);
this.seriesSizeName.forEach(() => {
this.seriesSizeRow.push([...values]);
});
this._canopiesSizeByYear = pageData.canopiesBySizeByYear.map((row: CanopyByYear) => {
const taille: string = row.taille.toString();
const year: string = row.year.toString();
this.seriesSizeRow[this.seriesSizeName.indexOf(taille)][this.seriesSizeHeader.indexOf(year)] = row.count;
return row;
});
}
private _loadCanopyModelBySize(pageData: CanopiesPageData): void {
this._canopiesModelBySize = pageData.canopiesBySizeByModel.map((row: CanopyModelBySize) => {
this.seriesModelName.push(`${row.taille.toString()} - ${row.voile}`);
this.seriesModelValue.push(row.count);
return row;
});
this._loadCanopyModelByYear(pageData);
}
private _loadCanopyModelByYear(pageData: CanopiesPageData): void {
this.seriesModelHeader = ['2018', '2019', '2020', '2021', '2022', '2023', '2024'];
const values: Array<number> = this.seriesModelHeader.map(() => 0);
this.seriesModelName.forEach(() => {
this.seriesModelRow.push([...values]);
});
this._canopiesModelByYear = pageData.canopiesBySizeByModelByYear.map((row: CanopyModelByYear) => {
const voile: string = row.voile;
const taille: string = row.taille.toString();
const year: string = row.year.toString();
this.seriesModelRow[this.seriesModelName.indexOf(`${taille} - ${voile}`)][this.seriesModelHeader.indexOf(year)] = row.count;
return row;
});
this.displayCharts = true;
}
}
@@ -0,0 +1,36 @@
<h1 class="mb-1">{{title}}</h1>
<span [hidden]="!lastJump.numero" class="me-2 fst-italic0 fs-6">
Dernier saut enregistré : <span class="fs-5 mx-1 text-primary">{{ lastJump.numero }}<sup>ème</sup></span> à <span class="fs-5 mx-1 text-primary">{{ lastJump.lieu }}</span> le <span class="fs-5 mx-1 text-primary">{{ lastJump.date | date: 'dd/MM/yyyy' }}</span>
</span>
<mat-divider class="my-3"></mat-divider>
<ng-container *appShowAuthed="true">
<div class="row">
<div class="col-xs-12">
<app-jumps-by-month></app-jumps-by-month>
</div>
</div>
<div class="row mt-sm-2 mt-lg-3">
<div class="col-md-4 col-sm-12">
<app-dropzones-pie></app-dropzones-pie>
</div>
<div class="col-md-8 col-sm-12">
<app-dropzones-bar></app-dropzones-bar>
</div>
</div>
<div class="row mt-sm-2 mt-lg-3">
<div class="col-md-4 col-sm-12">
<app-aeronefs-pie></app-aeronefs-pie>
</div>
<div class="col-md-8 col-sm-12">
<app-aeronefs-bar></app-aeronefs-bar>
</div>
</div>
<div class="row mt-sm-2 mt-lg-3">
<div class="col-md-6 col-sm-12">
<app-canopies-models></app-canopies-models>
</div>
<div class="col-md-6 col-sm-12">
<app-canopies-sizes></app-canopies-sizes>
</div>
</div>
</ng-container>
@@ -0,0 +1,21 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { DashboardComponent } from './dashboard.component';
describe('DashboardComponent', () => {
let component: DashboardComponent;
let fixture: ComponentFixture<DashboardComponent>;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [DashboardComponent]
});
fixture = TestBed.createComponent(DashboardComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,147 @@
import { Component, OnInit, OnDestroy } from '@angular/core';
import { DatePipe } from '@angular/common';
import { Title } from '@angular/platform-browser';
import { MatDividerModule } from '@angular/material/divider';
import { ActivatedRoute, Data, Router } from '@angular/router';
import { Observable, Subscription } from 'rxjs';
import { take } from 'rxjs/operators';
import { Aeronef, AeronefByImat, Canopy, CanopyModelBySize, DropZone, DropZoneByYear, Errors, Jump, User } from 'src/app/core/models';
//import { AeronefsService, CanopiesService, DropZonesService, UserService } from 'src/app/core/services';
import { JumpsService, UserService } from 'src/app/core/services';
import { ShowAuthedDirective } from 'src/app/components/shared/show-authed.directive';
import {
AeronefsPieComponent, AeronefsBarComponent,
CanopiesModelsComponent, CanopiesSizesComponent,
DropzonesPieComponent, DropzonesBarComponent,
JumpsByMonthComponent
} from 'src/app/components/shared/dashboard-components';
@Component({
standalone: true,
imports: [
DatePipe, ShowAuthedDirective, MatDividerModule,
AeronefsPieComponent, AeronefsBarComponent,
CanopiesModelsComponent, CanopiesSizesComponent,
DropzonesPieComponent, DropzonesBarComponent,
JumpsByMonthComponent
],
selector: 'app-dashboard',
templateUrl: './dashboard.component.html',
styleUrl: './dashboard.component.scss'
})
export class DashboardComponent implements OnInit, OnDestroy {
private _currentUser: Subscription = new Subscription();
private _data: Subscription = new Subscription();
private _lastjump: Subscription = new Subscription();
private _lastjump$: Observable<Jump> = new Observable();
/*
private _aeronefs: Subscription = new Subscription();
private _canopies: Subscription = new Subscription();
private _dropzones: Subscription = new Subscription();
*/
public title = 'Dashboard';
public errors!: Errors;
public isAuthenticated = false;
public canModify = false;
public aeronefAggregate!: Array<AeronefByImat>;
public aeronefs!: Array<Aeronef>;
public aeronefsCount = 0;
public canopyAggregate!: Array<CanopyModelBySize>;
public canopies!: Array<Canopy>;
public canopiesCount = 0;
public dropzoneAggregate!: Array<DropZoneByYear>;
public dropzones!: Array<DropZone>;
public dropzonesCount = 0;
public lastJump: Jump = {} as Jump;
constructor(
private route: ActivatedRoute,
private router: Router,
private titleService: Title,
private _userService: UserService,
private _jumpsService: JumpsService
/*
private aeronefsService: AeronefsService,
private canopiesService: CanopiesService,
private dropzonesService: DropZonesService
*/
) {
this._resetErrors();
}
ngOnInit() {
this.titleService.setTitle(this.title);
const data$: Observable<Data> = this.route.data.pipe(take(1));
this._data = data$.subscribe((data: Data) => {
this.isAuthenticated = data['isAuthenticated'];
if (!this.isAuthenticated) {
this.router.navigateByUrl('/login');
return;
}
const currentUser$: Observable<User> = this._userService.currentUser;
this._currentUser = currentUser$.subscribe((userData: User) => {
this.canModify = userData.role === 'Admin';
});
this._lastjump$ = this._jumpsService.getLastJump();
this._lastjump = this._lastjump$.subscribe((jump) => {
this.lastJump = jump;
});
});
/*
this._loadAeronefs();
this._loadCanopies();
this._loadDropzones();
*/
}
ngOnDestroy() {
this._currentUser.unsubscribe();
this._data.unsubscribe();
this._lastjump.unsubscribe();
/*
this._aeronefs.unsubscribe();
this._canopies.unsubscribe();
this._dropzones.unsubscribe();
*/
}
private _resetErrors(): void {
this.errors = { errors: {} };
}
/*
private _loadAeronefs(): void {
const aeronefs$: Observable<Array<AeronefByImat>> = this.aeronefsService.getAllByImat();
this._aeronefs = aeronefs$.subscribe((aggregate: Array<AeronefByImat>) => {
this.aeronefAggregate = aggregate;
this.aeronefsCount = aggregate.length;
console.log(this.aeronefAggregate);
console.log(this.aeronefsCount);
});
}
private _loadCanopies(): void {
const canopies$: Observable<Array<CanopyModelBySize>> = this.canopiesService.getAllModelBySize();
this._canopies = canopies$.subscribe((aggregate: Array<CanopyModelBySize>) => {
this.canopyAggregate = aggregate;
this.canopiesCount = aggregate.length;
console.log(this.canopyAggregate);
console.log(this.canopiesCount);
});
}
private _loadDropzones(): void {
const dropzones$: Observable<Array<DropZoneByYear>> = this.dropzonesService.getAllByDate();
this._dropzones = dropzones$.subscribe((aggregate: Array<DropZoneByYear>) => {
this.dropzoneAggregate = aggregate;
this.dropzonesCount = aggregate.length;
console.log(this.dropzoneAggregate);
console.log(this.dropzonesCount);
});
}
*/
}
@@ -0,0 +1,55 @@
<div @flyInOut>
<div class="d-flex">
<div class="flex-fill">
<h1 class="mb-1">{{title}}</h1>
<span [hidden]="!lastJump.numero" class="me-2 fs-6">
Dernier saut enregistré : <span class="fs-5 mx-1 text-primary">{{ lastJump.numero }}<sup>ème</sup></span> à <span class="fs-5 mx-1 text-primary">{{ lastJump.lieu }}</span> le <span class="fs-5 mx-1 text-primary">{{ lastJump.date | date: 'dd/MM/yyyy' }}</span>
</span>
</div>
<span class="flex-spacer"></span>
<div class="align-self-center me-2">
<button mat-icon-button [matMenuTriggerFor]="menuDropzone" aria-label="Menu Dropzone">
<mat-icon fontIcon="more_vert"></mat-icon>
</button>
<mat-menu #menuDropzone="matMenu">
@for (menuitem of menuItems.getMenuPanel(); track menuitem) {
<button mat-menu-item>
<mat-icon [fontIcon]="menuitem.icon"></mat-icon>
<span>{{ menuitem.name }}</span>
</button>
}
</mat-menu>
</div>
</div>
<mat-divider class="my-3"></mat-divider>
<mat-card>
<mat-card-header>
<mat-card-title>{{ subtitle }}</mat-card-title>
</mat-card-header>
<mat-card-content class="mt-4">
@if (displayCharts) {
<div class="row">
<div class="col-xl-4 col-lg-5 col-md-6 col-sm-12">
<app-pie-chart [headers]="seriesHeader" [names]="seriesName" [values]="seriesValue" [colors]="seriesColor"></app-pie-chart>
</div>
<div class="col-xl-8 col-lg-7 col-md-6 col-sm-12">
<app-bars-chart [headers]="seriesHeader" [names]="seriesName" [values]="seriesRow" [colors]="seriesColor" [legend]="false"></app-bars-chart>
<!--
@if (displayCharts) {
<div class="barchart position-relative w-100 my-1">
<x-chartist [configuration]="chartBarConfig"></x-chartist>
</div>
}
-->
</div>
</div>
}
<div class="d-flex">
<div class="flex-fill py-2">
<mat-divider class="mt-3 mb-0"></mat-divider>
<app-history-table [headers]="seriesHeader" [names]="seriesName" [values]="seriesValue" [rows]="seriesRow" [colors]="seriesColorClass"></app-history-table>
</div>
</div>
</mat-card-content>
</mat-card>
</div>
@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { DropzonesComponent } from './dropzones.component';
describe('DropzonesComponent', () => {
let component: DropzonesComponent;
let fixture: ComponentFixture<DropzonesComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [DropzonesComponent]
})
.compileComponents();
fixture = TestBed.createComponent(DropzonesComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,163 @@
import { DatePipe } from '@angular/common';
import { trigger, state, style, animate, transition } from '@angular/animations';
import { Component, DestroyRef, inject, OnInit, OnDestroy } from '@angular/core';
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { ActivatedRoute } from '@angular/router';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatDividerModule } from '@angular/material/divider';
import { MatExpansionModule } from '@angular/material/expansion';
import { MatGridListModule } from '@angular/material/grid-list';
import { MatIconModule } from '@angular/material/icon';
import { MatMenuModule } from '@angular/material/menu';
//import { ChartistModule, Configuration } from 'ng-chartist';
//import { AxisOptions, Label } from 'chartist';
import { Observable, Subscription } from 'rxjs';
import { MenuItems } from 'src/app/components/shared';
import { BarsChartComponent, PieChartComponent } from 'src/app/components/shared/helpers-chart';
import { HistoryTableComponent } from 'src/app/components/shared/helpers-jump';
import { DropZoneByOaci, DropZoneByYear, DropZonesPageData, Jump } from 'src/app/core/models';
import { UtilitiesService } from 'src/app/core/services';
@Component({
selector: 'app-dropzones',
standalone: true,
imports: [
DatePipe,
MatButtonModule, MatCardModule, MatDividerModule, MatExpansionModule,
MatGridListModule, MatIconModule, MatMenuModule,
//ChartistModule,
HistoryTableComponent, BarsChartComponent, PieChartComponent
],
templateUrl: './dropzones.component.html',
styleUrl: './dropzones.component.scss',
animations: [
trigger('flyInOut', [
state('in', style({ transform: 'translateX(0)' })),
transition('void => *', [
style({ transform: 'translateX(-100%)' }),
animate(200)
]),
transition('* => void', [
style({ transform: 'translateX(100%)' }),
animate(200)
])
])
]
})
export class DropzonesComponent implements OnInit, OnDestroy {
private _data: Subscription = new Subscription();
private _lastjump: Subscription = new Subscription();
private _dropzonesByOaci!: Array<DropZoneByOaci>;
private _dropzonesByYear!: Array<DropZoneByYear>;
public lastJump: Jump = {} as Jump;
public title = 'Dropzones';
public subtitle: string = 'Nombre total de sauts par dropzone';
public displayCharts = false;
public destroyRef = inject(DestroyRef);
public seriesHeader: string[] = [];
public seriesName: string[] = [];
public seriesValue: number[] = [];
public seriesRow: Array<Array<number>> = [];
public seriesColor: {
backgroundColor: string[],
borderColor: string[]
} = {
backgroundColor: this._utilitiesService.getSeriesColors(0.8, 'all'),
borderColor: this._utilitiesService.getSeriesColors(1, 'all')
};
public seriesColorClass: string[] = this._utilitiesService.getChartColors();
//public chartBarConfig: Configuration = this._utilitiesService.getBarConfig();
constructor(
private route: ActivatedRoute,
private _utilitiesService: UtilitiesService,
public menuItems: MenuItems
) { }
ngOnInit() {
const data$: Observable<{ dropZonesPageData: DropZonesPageData }> = this.route.data as Observable<{ dropZonesPageData: DropZonesPageData }>;
this._data = data$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((data: { dropZonesPageData: DropZonesPageData }) => {
const pageData: DropZonesPageData = data.dropZonesPageData;
this.lastJump = pageData.lastjump;
this._loadDropZoneByOaci(pageData);
});
}
ngOnDestroy() {
this._data.unsubscribe();
}
private _loadDropZoneByOaci(pageData: DropZonesPageData): void {
this._dropzonesByOaci = pageData.dropZonesByOaci.map((row: DropZoneByOaci) => {
this.seriesName.push(`${row.oaci} - ${row.lieu}`);
this.seriesValue.push(row.count);
return row;
});
this._loadDropZoneByYear(pageData);
}
private _loadDropZoneByYear(pageData: DropZonesPageData): void {
this.seriesHeader = ['2018', '2019', '2020', '2021', '2022', '2023', '2024'];
const values: Array<number> = this.seriesHeader.map(() => 0);
this.seriesName.forEach(() => {
this.seriesRow.push([...values]);
});
this._dropzonesByYear = pageData.dropZonesByOaciByYear.map((row: DropZoneByYear) => {
const lieu: string = row.lieu;
const oaci: string = row.oaci;
const year: string = row.year.toString();
this.seriesRow[this.seriesName.indexOf(`${oaci} - ${lieu}`)][this.seriesHeader.indexOf(year)] = row.count;
return row;
});
/*
const axisX: AxisOptions = {
labelInterpolationFnc: function(value: Label, index: number): string {
return index % 1 === 0 ? `${value.toString()}` : '';
}
}
this.chartBarConfig = {
type: 'Bar',
data: {
'labels': this.seriesHeader,
'series': this.seriesRow
},
options: {
seriesBarDistance: 15,
horizontalBars: false,
high: 180,
axisX: {
showGrid: false,
offset: 20
},
axisY: {
showGrid: true,
offset: 40
},
height: 300
},
responsiveOptions: [
['screen and (min-width: 1024px)', {
axisX: axisX
}],
['screen and (min-width: 641px) and (max-width: 1024px)', {
seriesBarDistance: 7,
axisY: {
offset: 30
},
axisX: axisX
}],
['screen and (max-width: 640px)', {
seriesBarDistance: 5,
axisY: {
offset: 20
},
axisX: axisX
}]
]
};
*/
this.displayCharts = true;
}
}
@@ -1,45 +0,0 @@
<mat-card>
<mat-card-content class="mb-0">
<mat-card-title>Aéronefs</mat-card-title>
<mat-card-subtitle>Nombre total de sauts par avions</mat-card-subtitle>
<div class="barchrt">
<x-chartist class="" [data]="barChartAeronef.data" [type]="barChartAeronef.type" [options]="barChartAeronef.options" [responsiveOptions]="barChartAeronef.responsiveOptions"
[events]="barChartAeronef.events"> </x-chartist>
</div>
</mat-card-content>
<hr>
<mat-card-content>
<table class="table table-striped table-dark table-hover">
<thead>
<tr>
<th></th>
<th *ngFor='let name of seriesHeader; let i=index' class="text-right"> {{name}} </th>
<th class="text-right">Total</th>
</tr>
</thead>
<tbody class="text-mono">
<tr *ngFor='let values of seriesRow; let i=index'>
<th class="{{seriesColor[i]}} text-right"><i class="mdi mdi-checkbox-blank-circle font-10 m-r-10 "></i> {{seriesName[i]}} </th>
<td *ngFor='let value of values; let i=index' class="text-right">
<span *ngIf="value; else elseBlock" class="pr-1">{{value}}</span>
<ng-template #elseBlock><span class="text-empty pr-1">{{value}}</span></ng-template>
</td>
<th class="{{seriesColor[i]}} text-right">
<span *ngIf="seriesValue[i]; else elseBlock" class="pr-1">{{seriesValue[i]}}</span>
<ng-template #elseBlock><span class="text-empty pr-1">{{seriesValue[i]}}</span></ng-template>
</th>
</tr>
</tbody>
</table>
</mat-card-content>
<!--<hr>
<mat-card-content>
<div fxLayout="row wrap">
<div *ngFor='let name of seriesName; let i=index' fxFlex.gt-lg="33" fxFlex.gt-md="50" fxFlex.gt-xs="20" fxFlex="100">
<h6 class="{{seriesColor[i]}} m-0">
<i class="mdi mdi-checkbox-blank-circle font-10 m-r-10 "></i>{{name}} - {{seriesValue[i]}}
</h6>
</div>
</div>
</mat-card-content>-->
</mat-card>
@@ -1,128 +0,0 @@
import { Component, OnInit} from '@angular/core';
import { CommonModule } from '@angular/common';
import { MatCardModule } from '@angular/material/card';
import { ChartistModule, Configuration } from 'ng-chartist';
import { BackendService } from 'src/app/core/services';
import { ReduceRow } from 'src/app/core/models';
@Component({
standalone: true,
imports: [CommonModule, MatCardModule, ChartistModule],
selector: 'huapp-aeronefs-bar',
templateUrl: './aeronefs-bar.component.html'
})
export class AeronefsBarComponent implements OnInit {
rows: Object[] = [] as Array<Object>;
seriesName: string[] = [];
seriesValue: any[] = [];
seriesColor: string[] = this._dataService.getChartColors();
seriesHeader: string[] = [];
seriesRow: any[] = [];
barChartAeronef: Configuration = {
type: 'Bar',
data: {
"labels": [],
"series": []
},
options: {},
responsiveOptions: []
};
constructor(private _dataService: BackendService) { }
loadDataByDate() {
return this._dataService
.getAeronefsByDate(2)
.subscribe((res) => {
this.seriesName.forEach((name: string) => {
this.seriesRow.push([0, 0, 0, 0, 0, 0]);
});
this.seriesHeader = ["2018", "2019", "2020", "2021", "2022", "2023"];
res["rows"].forEach((row: ReduceRow) => {
let year = row.key[1].toString();
let avion = row.key[0];
this.seriesRow[this.seriesName.indexOf(avion)][this.seriesHeader.indexOf(year)] = row.value;
});
return true;
});
}
ngOnInit() {
return this._dataService
.getAeronefs()
.subscribe((res) => {
this.rows = res["rows"]
this.rows.forEach((row: ReduceRow) => {
this.seriesName.push(row.key[0] + ' ' + row.key[1]);
this.seriesValue.push([row.value]);
});
this.barChartAeronef = {
type: 'Bar',
data: {
"labels": this.seriesName,
"series": this.seriesValue
},
options: {
chartPadding: {
top: 15,
right: 20,
bottom: 0,
left: 20
},
seriesBarDistance: 15,
horizontalBars: true,
axisX: {
position: 'end',
labelOffset: {
x: -5,
y: 0
},
showGrid: true,
offset: 20
},
axisY: {
showGrid: false,
offset: 0,
labelInterpolationFnc: function(): string {
return '';
}
},
height: (this.seriesName.length*20)
},
responsiveOptions: [
['screen and (min-width: 1024px)', {
axisX: {
labelInterpolationFnc: function(value: number, index: number): string {
return index % 1 === 0 ? `${value}` : '';
}
}
}],
['screen and (min-width: 641px) and (max-width: 1024px)', {
seriesBarDistance: 10,
axisY: {
offset: 30
},
axisX: {
labelInterpolationFnc: function(value: any, index: number): string {
return index % 1 === 0 ? `${value[2]}${value[3]}` : '';
}
}
}],
['screen and (max-width: 640px)', {
seriesBarDistance: 5,
axisY: {
offset: 20
},
axisX: {
labelInterpolationFnc: function(value: any, index: number): string {
return index % 1 === 0 ? `${value[2]}${value[3]}` : '';
}
}
}]
]
};
return this.loadDataByDate();
});
}
}
@@ -1,44 +0,0 @@
<mat-card>
<mat-card-content>
<mat-card-title>Aéronefs</mat-card-title>
<mat-card-subtitle>Nombre total de sauts par avions</mat-card-subtitle>
<div class="piechart">
<x-chartist [configuration]="donuteChartModel"> </x-chartist>
</div>
</mat-card-content>
<hr>
<mat-card-content>
<table class="table table-striped table-dark table-hover">
<thead>
<tr>
<th></th>
<th *ngFor='let name of seriesHeader; let i=index' class="text-right"> {{name}} </th>
<th class="text-right">Total</th>
</tr>
</thead>
<tbody>
<tr *ngFor='let values of seriesRow; let i=index'>
<th class="{{seriesColor[i]}} text-right"><i class="mdi mdi-checkbox-blank-circle font-10 m-r-10 "></i> {{seriesName[i]}} </th>
<td *ngFor='let value of values; let i=index' class="text-mono text-right">
<span *ngIf="value; else elseBlock" class="pr-1">{{value}}</span>
<ng-template #elseBlock><span class="text-empty pr-1">{{value}}</span></ng-template>
</td>
<th class="{{seriesColor[i]}} text-right">
<span *ngIf="seriesValue[i]; else elseBlock" class="pr-1">{{seriesValue[i]}}</span>
<ng-template #elseBlock><span class="text-empty pr-1">{{seriesValue[i]}}</span></ng-template>
</th>
</tr>
</tbody>
</table>
</mat-card-content>
<!--<hr>
<mat-card-content>
<div fxLayout="row wrap">
<div *ngFor='let name of seriesName; let i=index' fxFlex.gt-lg="33" fxFlex.gt-md="50" fxFlex.gt-xs="20" fxFlex="100">
<h6 class="{{seriesColor[i]}} m-0">
<i class="mdi mdi-checkbox-blank-circle font-10 m-r-10 "></i>{{name}} - {{seriesValue[i]}}
</h6>
</div>
</div>
</mat-card-content>-->
</mat-card>
@@ -1,135 +0,0 @@
import { Component, OnInit} from '@angular/core';
import { CommonModule } from '@angular/common';
import { MatCardModule } from '@angular/material/card';
import { ChartistModule, Configuration } from 'ng-chartist';
import { Observable, Subscription } from 'rxjs';
import { BackendService, AeronefsService, UserService } from 'src/app/core/services';
import { Aeronef, AeronefAggregate } from 'src/app/core/models';
@Component({
standalone: true,
imports: [CommonModule, MatCardModule, ChartistModule],
selector: 'huapp-aeronefs',
templateUrl: './aeronefs.component.html'
})
export class AeronefsComponent implements OnInit {
private _aeronefs: Subscription = new Subscription();
aggregate!: Array<AeronefAggregate>
aeronefs!: Array<Aeronef>;
aeronefsCount = 0;
rows: Object[] = [] as Array<Object>;
seriesName: string[] = [];//= ["Arcachon", "Béni-Mellal", "La Réole", "Royan", "Pamiers", "Soulac", "Sables d'Olonne", "Rochefort"];
seriesValue: number[] = [];
seriesColor: string[] = this._dataService.getChartColors(); //["blue", "red", "turquoise", "orange", "cyan", "purple-dark", "megna", "raspberry"];
seriesHeader: string[] = [];
seriesRow: any[] = [];
donuteChartModel: Configuration = {
type: 'Pie',
data: {
"labels": [],
"series": []
},
options: {
donut: true,
height: 260,
donutWidth: 40,
startAngle: 270,
showLabel: true
}
};
constructor(
private aeronefsService: AeronefsService,
private _dataService: BackendService
) { }
/*loadDataByDate() {
return this._dataService
.getAeronefsByDate(2)
.subscribe((res) => {
this.seriesName.forEach((name: string) => {
this.seriesRow.push([0, 0, 0, 0, 0, 0]);
});
this.seriesHeader = ["2018", "2019", "2020", "2021", "2022", "2023"];
res["rows"].forEach((row: ReduceRow) => {
let year = row.key[1].toString();
let avion = row.key[0];
this.seriesRow[this.seriesName.indexOf(avion)][this.seriesHeader.indexOf(year)] = row.value;
});
return true;
});
}*/
ngOnInit() {
this.loadAeronefs();
return this._dataService
.getAeronefs()
.subscribe((res) => {
console.log(res);
/* this.rows = res["rows"]
var labels: string[] = [];
var series: number[] = [];
this.rows.forEach((row: ReduceRow) => {
this.seriesName.push(row.key[0] + ' ' + row.key[1]);
this.seriesValue.push(row.value);
//console.log(row.key, row.value);
labels.push(row.value.toString());
series.push(row.value);
});
this.donuteChartModel = {
type: 'Pie',
data: {
"labels": labels,
"series": series
},
options: {
donut: true,
height: 260,
donutWidth: 40,
startAngle: 270,
showLabel: true
}
//,
// events: <ChartEvent>{
// draw: (data) => {
// //this.donuteChartModel.data = data;
// console.log(data);
// }
// }
};
return this.loadDataByDate(); */
});
}
loadAeronefs() {
const aeronefs$: Observable<Array<AeronefAggregate>> = this.aeronefsService.getAll();
this._aeronefs = aeronefs$.subscribe((aggregate: Array<AeronefAggregate>) => {
this.aggregate = aggregate;
this.aeronefsCount = aggregate.length;
var labels: string[] = [];
var series: number[] = [];
this.aggregate.forEach((row) => {
this.seriesName.push(row._id.aeronef + ' ' + row._id.imat);
this.seriesValue.push(row.count);
labels.push(row.count.toString());
//labels.push(row._id.aeronef + ' ' + row._id.imat);
series.push(row.count);
});
this.donuteChartModel = {
type: 'Pie',
data: {
"labels": labels,
"series": series
},
options: {
donut: true,
height: 260,
donutWidth: 50,
startAngle: 270,
showLabel: true
}
};
});
}
}
@@ -1,34 +0,0 @@
<mat-card>
<mat-card-content>
<mat-card-title>Modèles de voile</mat-card-title>
<mat-card-subtitle>Nombre total de sauts par modèle</mat-card-subtitle>
<div class="piechart">
<x-chartist [configuration]="donuteCanopyModel"> </x-chartist>
</div>
</mat-card-content>
<hr>
<mat-card-content>
<table class="table table-striped table-dark table-hover">
<thead>
<tr>
<th></th>
<th *ngFor='let name of seriesHeader; let i=index' class="text-right"> {{name}} </th>
<th class="text-right">Total</th>
</tr>
</thead>
<tbody class="text-mono">
<tr *ngFor='let values of seriesRow; let i=index'>
<th class="{{seriesColor[i]}} text-right"><i class="mdi mdi-checkbox-blank-circle font-10 m-r-10 "></i> {{seriesName[i]}} </th>
<td *ngFor='let value of values; let i=index' class="text-right">
<span *ngIf="value; else elseBlock" class="pr-1">{{value}}</span>
<ng-template #elseBlock><span class="text-empty pr-1">{{value}}</span></ng-template>
</td>
<th class="{{seriesColor[i]}} text-right">
<span *ngIf="seriesValue[i]; else elseBlock" class="pr-1">{{seriesValue[i]}}</span>
<ng-template #elseBlock><span class="text-empty pr-1">{{seriesValue[i]}}</span></ng-template>
</th>
</tr>
</tbody>
</table>
</mat-card-content>
</mat-card>
@@ -1,133 +0,0 @@
import { Component, OnInit} from '@angular/core';
import { CommonModule } from '@angular/common';
import { MatCardModule } from '@angular/material/card';
import { ChartistModule, Configuration } from 'ng-chartist';
import { Observable, Observer, Subscription } from 'rxjs';
import { take } from 'rxjs/operators';
import { BackendService, CanopiesService } from 'src/app/core/services';
import { Canopy, CanopyAggregate, ReduceRow } from 'src/app/core/models';
@Component({
standalone: true,
imports: [CommonModule, MatCardModule, ChartistModule],
selector: 'huapp-canopy-models',
templateUrl: './canopy-models.component.html'
})
export class CanopyModelsComponent implements OnInit {
private _canopies: Subscription = new Subscription();
aggregate!: Array<CanopyAggregate>
canopies!: Array<Canopy>;
canopiesCount = 0;
rows: Object[] = [] as Array<Object>;
seriesName: string[] = [];
seriesValue: number[] = [];
seriesColor: string[] = this._dataService.getChartColors();
seriesHeader: string[] = [];
seriesRow: any[] = [];
donuteCanopyModel: Configuration = {
type: 'Pie',
data: {
"labels": [],
"series": []
},
options: {
donut: true,
height: 260,
donutWidth: 40,
startAngle: 270,
showLabel: true
}
};
constructor(
private canopiesService: CanopiesService,
private _dataService: BackendService
) { }
/*
loadDataByDate() {
return this._dataService
.getCanopyModelsByDate(2)
.subscribe((res) => {
this.seriesName.forEach((name: string) => {
this.seriesRow.push([0, 0, 0, 0, 0, 0]);
});
this.seriesHeader = ["2018", "2019", "2020", "2021", "2022", "2023"];
res["rows"].forEach((row: ReduceRow) => {
let year = row.key[1].toString();
let voile = row.key[0];
this.seriesRow[this.seriesName.indexOf(voile)][this.seriesHeader.indexOf(year)] = row.value;
});
return true;
});
}
*/
ngOnInit() {
this.loadCanopies();
/*
return this._dataService
.getCanopyModels()
.subscribe((res) => {
this.rows = res["rows"]
var labels: string[] = [];
var series: number[] = [];
this.rows.forEach((row: ReduceRow) => {
this.seriesName.push(row.key);
this.seriesValue.push(row.value);
labels.push(row.value.toString());
series.push(row.value);
});
this.donuteCanopyModel = {
type: 'Pie',
data: {
"labels": labels,
"series": series
},
options: {
donut: true,
height: 260,
donutWidth: 40,
donutSolid: true,
startAngle: 270,
showLabel: true
}
};
return this.loadDataByDate();
});
*/
}
loadCanopies() {
const canopies$: Observable<Array<CanopyAggregate>> = this.canopiesService.getAll();
this._canopies = canopies$.subscribe((aggregate: Array<CanopyAggregate>) => {
this.aggregate = aggregate;
//this.canopies = canopies;
this.canopiesCount = aggregate.length;
var labels: string[] = [];
var series: number[] = [];
this.aggregate.forEach((row) => {
this.seriesName.push(row._id.canopy + ' ' + row._id.taille);
this.seriesValue.push(row.count);
labels.push(row.count.toString());
series.push(row.count);
});
this.donuteCanopyModel = {
type: 'Pie',
data: {
"labels": labels,
"series": series
},
options: {
donut: true,
height: 260,
donutWidth: 50,
startAngle: 270,
showLabel: true
}
};
});
}
}
@@ -1,43 +0,0 @@
<mat-card>
<mat-card-content>
<mat-card-title>Taille de voile</mat-card-title>
<mat-card-subtitle>Nombre total de sauts par taille</mat-card-subtitle>
<div class="piechart">
<x-chartist class="" [data]="donuteChartSize.data" [type]="donuteChartSize.type"
[options]="donuteChartSize.options" [responsiveOptions]="donuteChartSize.responsiveOptions"
[events]="donuteChartSize.events"> </x-chartist>
</div>
</mat-card-content>
<hr>
<mat-card-content>
<table class="table table-striped table-dark table-hover">
<thead>
<tr>
<th></th>
<th *ngFor='let name of seriesHeader; let i=index' class="text-right"> {{name}} </th>
<th class="text-right">Total</th>
</tr>
</thead>
<tbody class="text-mono">
<tr *ngFor='let values of seriesRow; let i=index'>
<th class="{{seriesColor[i]}} text-right"><i class="mdi mdi-checkbox-blank-circle font-10 m-r-10 "></i> {{seriesName[i]}} </th>
<td *ngFor='let value of values; let i=index' class="text-right">
<span *ngIf="value; else elseBlock" class="pr-1">{{value}}</span>
<ng-template #elseBlock><span class="text-empty pr-1">{{value}}</span></ng-template>
</td>
<th class="{{seriesColor[i]}} text-right">
<span *ngIf="seriesValue[i]; else elseBlock" class="pr-1">{{seriesValue[i]}}</span>
<ng-template #elseBlock><span class="text-empty pr-1">{{seriesValue[i]}}</span></ng-template>
</th>
</tr>
</tbody>
</table>
<!--<div fxLayout="row wrap">
<div fxLayout="row wrap">
<h4 class="{{seriesColor[i]}} m-0" *ngFor='let name of seriesName; let i=index' fxFlex.gt-lg="25" fxFlex.gt-md="50" fxFlex.gt-sm="50" fxFlex.gt-xs="25" fxFlex="100">
<i class="mdi mdi-checkbox-blank-circle font-10 m-r-10 "></i>{{name}} ft² : {{seriesValue[i]}}
</h4>
</div>
</div>-->
</mat-card-content>
</mat-card>
@@ -1,87 +0,0 @@
import { Component, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { MatCardModule } from '@angular/material/card';
import { ChartistModule, Configuration } from 'ng-chartist';
import { BackendService } from 'src/app/core/services';
import { ReduceRow } from 'src/app/core/models';
@Component({
standalone: true,
imports: [CommonModule, MatCardModule, ChartistModule],
selector: 'huapp-canopy-sizes',
templateUrl: './canopy-sizes.component.html'
})
export class CanopySizesComponent implements OnInit {
rows: Object[] = [] as Array<Object>;
seriesName: string[] = [];
seriesValue: number[] = [];
seriesColor: string[] = this._dataService.getChartColors();
seriesHeader: string[] = [];
seriesRow: any[] = [];
donuteChartSize: Configuration = {
type: 'Pie',
data: {
"labels": [],
"series": []
},
options: {
donut: true,
height: 260,
donutWidth: 40,
startAngle: 0,
showLabel: true
}
};
constructor(private _dataService: BackendService) { }
loadDataByDate() {
return this._dataService
.getCanopySizesByDate(2)
.subscribe((res) => {
this.seriesName.forEach((name: string) => {
this.seriesRow.push([0, 0, 0, 0, 0, 0]);
});
this.seriesHeader = ["2018", "2019", "2020", "2021", "2022", "2023"];
res["rows"].forEach((row: ReduceRow) => {
let year = row.key[1].toString();
let size = row.key[0];
this.seriesRow[this.seriesName.indexOf(size)][this.seriesHeader.indexOf(year)] = row.value;
});
return true;
});
}
ngOnInit() {
return this._dataService
.getCanopySizes()
.subscribe((res) => {
this.rows = res["rows"]
var labels: string[] = [];
var series: number[] = [];
this.rows.forEach((row: ReduceRow) => {
this.seriesName.push(row.key);
this.seriesValue.push(row.value);
labels.push(row.value.toString());
series.push(row.value);
});
this.donuteChartSize = {
type: 'Pie',
data: {
"labels": labels,
"series": series
},
options: {
donut: true,
height: 260,
donutWidth: 40,
donutSolid: true,
startAngle: 330,
showLabel: true
}
};
return this.loadDataByDate();
});
}
}
@@ -1,25 +0,0 @@
<mat-card>
<mat-card-content>
<mat-card-title>Les drop zones</mat-card-title>
<mat-card-subtitle>Nombre total de sauts par DZ</mat-card-subtitle>
<div class="piechart">
<x-chartist [configuration]="donuteDropZones"> </x-chartist>
</div>
</mat-card-content>
<hr>
<mat-card-content class="mb-1">
<div fxLayout="row wrap">
<div fxLayout="row wrap">
<h4 class="{{seriesColor[i]}} m-0" *ngFor='let name of seriesName; let i=index' fxFlex.gt-lg="33" fxFlex.gt-md="50" fxFlex.gt-sm="100" fxFlex.gt-xs="25" fxFlex="100">
<i class="mdi mdi-checkbox-blank-circle font-10 m-r-10 "></i>{{name}} : {{seriesValue[i]}}
</h4>
</div>
</div>
</mat-card-content>
<hr>
<mat-card-content>
<div class="barchrt bar-xs">
<x-chartist [configuration]="barChartDropZones"> </x-chartist>
</div>
</mat-card-content>
</mat-card>
@@ -1,170 +0,0 @@
import { Component, OnInit, Input, Output } from '@angular/core';
import { CommonModule } from '@angular/common';
import { MatCardModule } from '@angular/material/card';
import { ChartistModule, Configuration } from 'ng-chartist';
import { BackendService } from 'src/app/core/services';
import { DropZone, ReduceRow} from 'src/app/core/models';
@Component({
standalone: true,
imports: [CommonModule, MatCardModule, ChartistModule],
selector: 'huapp-drop-zones-bar',
templateUrl: './drop-zones-bar.component.html'
})
export class DropZonesBarComponent implements OnInit {
dropzones: Object[] = [] as Array<Object>;
seriesName: string[] = [];//= ["Arcachon", "Béni-Mellal", "La Réole", "Royan", "Pamiers", "Soulac", "Sables d'Olonne", "Rochefort"];
seriesValue: number[] = [];
seriesColor: string[] = this._dataService.getChartColors();
seriesHeader: string[] = [];
seriesRow: any[] = [];
donuteDropZones: Configuration = {
type: 'Pie',
/*
data: {
"labels": ["261", "45", "178", "16", "13", "11", "4", "2"],
"series": [261, 45, 178, 16, 13, 11, 4, 2]
},
*/
data: {
"labels": [],
"series": []
},
options: {
donut: true,
height: 260,
donutWidth: 40,
startAngle: 270,
showLabel: true
}
};
barChartDropZones: Configuration = {
type: 'Bar',
data: {
"labels": [],
"series": []
},
options: {},
responsiveOptions: []
};
constructor(private _dataService: BackendService) { }
loadDataByDate() {
return this._dataService
.getDropZonesByDate(2)
.subscribe((res) => {
console.log(res["rows"]);
this.seriesName.forEach((name: string) => {
this.seriesRow.push([0, 0, 0, 0, 0, 0]);
});
this.seriesHeader = ["2018", "2019", "2020", "2021", "2022", "2023"];
res["rows"].forEach((row: ReduceRow) => {
let year = row.key[1].toString();
let lieu = row.key[0];
this.seriesRow[this.seriesName.indexOf(lieu)][this.seriesHeader.indexOf(year)] = row.value;
//console.log(row.key, row.value);
});
//console.log(this.seriesName);
//console.log(this.seriesHeader);
//console.log(this.seriesRow);
this.barChartDropZones = {
type: 'Bar',
data: {
"labels": this.seriesHeader,
"series": this.seriesRow
},
options: {
seriesBarDistance: 9,
horizontalBars: true,
high: 170,
axisX: {
showGrid: false,
offset: 20
},
axisY: {
showGrid: true,
offset: 40
},
height: 207
},
responsiveOptions: [
['screen and (min-width: 1024px)', {
axisX: {
labelInterpolationFnc: function(value: number, index: number): string {
return index % 1 === 0 ? `${value}` : '';
}
}
}],
['screen and (min-width: 641px) and (max-width: 1024px)', {
seriesBarDistance: 7,
axisY: {
offset: 30
},
axisX: {
labelInterpolationFnc: function(value: any, index: number): string {
return index % 1 === 0 ? `${value[2]}${value[3]}` : '';
}
}
}],
['screen and (max-width: 640px)', {
seriesBarDistance: 5,
axisY: {
offset: 20
},
axisX: {
labelInterpolationFnc: function(value: any, index: number): string {
return index % 1 === 0 ? `${value[2]}${value[3]}` : '';
}
}
}]
]
};
return true;
});
}
ngOnInit() {
return this._dataService
.getDropZones()
.subscribe((res) => {
//console.log('ngOnInit');
//console.log(res["rows"]);
this.dropzones = res["rows"]
var labels: string[] = [];
var series: number[] = [];
this.dropzones.forEach((dropzone: DropZone) => {
this.seriesName.push(dropzone.key);
this.seriesValue.push(dropzone.value);
//console.log(dropzone.key, dropzone.value);
labels.push(dropzone.value.toString());
series.push(dropzone.value);
});
this.donuteDropZones = {
type: 'Pie',
data: {
"labels": labels,
"series": series
},
options: {
donut: true,
height: 260,
donutWidth: 40,
donutSolid: true,
startAngle: 270,
showLabel: true
}
/*,
events: <ChartEvent>{
draw: (data) => {
//this.donuteDropZones.data = data;
console.log(data);
}
}
*/
};
return this.loadDataByDate();
});
}
}
@@ -1,34 +0,0 @@
<mat-card>
<mat-card-content>
<mat-card-title>Les drop zones</mat-card-title>
<mat-card-subtitle>Nombre total de sauts par DZ</mat-card-subtitle>
<div class="piechart">
<x-chartist [configuration]="donuteDropZones"> </x-chartist>
</div>
</mat-card-content>
<hr>
<mat-card-content>
<table class="table table-striped table-dark table-hover">
<thead>
<tr>
<th></th>
<th *ngFor='let name of seriesHeader; let i=index' class="text-right"> {{name}} </th>
<th class="text-right">Total</th>
</tr>
</thead>
<tbody class="text-mono">
<tr *ngFor='let values of seriesRow; let i=index'>
<th class="{{seriesColor[i]}} text-right"><i class="mdi mdi-checkbox-blank-circle font-10 m-r-10 "></i> {{seriesName[i]}} </th>
<td *ngFor='let value of values; let i=index' class="text-right">
<span *ngIf="value; else elseBlock" class="pr-1">{{value}}</span>
<ng-template #elseBlock><span class="text-empty pr-1">{{value}}</span></ng-template>
</td>
<th class="{{seriesColor[i]}} text-right">
<span *ngIf="seriesValue[i]; else elseBlock" class="pr-1">{{seriesValue[i]}}</span>
<ng-template #elseBlock><span class="text-empty pr-1">{{seriesValue[i]}}</span></ng-template>
</th>
</tr>
</tbody>
</table>
</mat-card-content>
</mat-card>
@@ -1,187 +0,0 @@
import { Component, OnInit, Input, Output } from '@angular/core';
import { CommonModule } from '@angular/common';
import { MatCardModule } from '@angular/material/card';
import { ChartistModule, Configuration } from 'ng-chartist';
import { Observable, Observer, Subscription } from 'rxjs';
import { BackendService, DropZonesService } from 'src/app/core/services';
import { DropZone, DropZoneAggregate, ReduceRow} from 'src/app/core/models';
@Component({
standalone: true,
imports: [CommonModule, MatCardModule, ChartistModule],
selector: 'huapp-drop-zones',
templateUrl: './drop-zones.component.html'
})
export class DropZonesComponent implements OnInit {
private _dropzones: Subscription = new Subscription();
aggregate!: Array<DropZoneAggregate>
dropzones!: Array<DropZone>;
dropzonesCount = 0;
seriesName: string[] = [];//= ["Arcachon", "Béni-Mellal", "La Réole", "Royan", "Pamiers", "Soulac", "Sables d'Olonne", "Rochefort"];
seriesValue: number[] = [];
seriesColor: string[] = this._dataService.getChartColors();
seriesHeader: string[] = [];
seriesRow: any[] = [];
donuteDropZones: Configuration = {
type: 'Pie',
/*
data: {
"labels": ["261", "45", "178", "16", "13", "11", "4", "2"],
"series": [261, 45, 178, 16, 13, 11, 4, 2]
},
*/
data: {
"labels": [],
"series": []
},
options: {
donut: true,
height: 260,
donutWidth: 40,
startAngle: 270,
showLabel: true
}
};
barChartDropZones: Configuration = {
type: 'Bar',
data: {
"labels": [],
"series": []
},
options: {
seriesBarDistance: 5,
high: 170,
axisX: {
showGrid: false,
offset: 20
},
axisY: {
showGrid: true,
offset: 40
},
height: 207
},
responsiveOptions: [
['screen and (min-width: 1024px)', {
axisX: {
labelInterpolationFnc: function(value: number, index: number): string {
return index % 1 === 0 ? `${value}` : '';
}
}
}],
['screen and (min-width: 641px) and (max-width: 1024px)', {
seriesBarDistance: 10,
axisY: {
offset: 30
},
axisX: {
labelInterpolationFnc: function(value: any, index: number): string {
return index % 1 === 0 ? `${value[2]}${value[3]}` : '';
}
}
}],
['screen and (max-width: 640px)', {
seriesBarDistance: 5,
axisY: {
offset: 20
},
axisX: {
labelInterpolationFnc: function(value: any, index: number): string {
return index % 1 === 0 ? `${value[2]}${value[3]}` : '';
}
}
}]
]
};
constructor(
private dropzonesService: DropZonesService,
private _dataService: BackendService
) { }
ngOnInit() {
this.loadDropZones();
/*
return this._dataService
.getDropZones()
.subscribe((res) => {
this.dropzones = res["rows"]
var labels: string[] = [];
var series: number[] = [];
this.dropzones.forEach((dropzone: DropZone) => {
this.seriesName.push(dropzone.key);
this.seriesValue.push(dropzone.value);
labels.push(dropzone.value.toString());
series.push(dropzone.value);
});
this.donuteDropZones = {
type: 'Pie',
data: {
"labels": labels,
"series": series
},
options: {
donut: true,
height: 260,
donutWidth: 40,
donutSolid: true,
startAngle: 270,
showLabel: true
}
,
// events: <ChartEvent>{
// draw: (data) => {
// //this.donuteDropZones.data = data;
// console.log(data);
// }
// }
};
return this._dataService
.getDropZonesByDate(2)
.subscribe((res) => {
this.seriesName.forEach((name: string) => {
this.seriesRow.push([0, 0, 0, 0, 0, 0]);
});
this.seriesHeader = ["2018", "2019", "2020", "2021", "2022", "2023"];
res["rows"].forEach((row: ReduceRow) => {
let year = row.key[1].toString();
let lieu = row.key[0];
this.seriesRow[this.seriesName.indexOf(lieu)][this.seriesHeader.indexOf(year)] = row.value;
});
return true;
});
});
*/
}
loadDropZones() {
const dropzones$: Observable<Array<DropZoneAggregate>> = this.dropzonesService.getAll();
this._dropzones = dropzones$.subscribe((aggregate: Array<DropZoneAggregate>) => {
this.aggregate = aggregate;
//this.dropzones = dropzones;
this.dropzonesCount = aggregate.length;
var labels: string[] = [];
var series: number[] = [];
this.aggregate.forEach((row) => {
this.seriesName.push(row._id.lieu + ' ' + row._id.oaci);
this.seriesValue.push(row.count);
labels.push(row.count.toString());
series.push(row.count);
});
this.donuteDropZones = {
type: 'Pie',
data: {
"labels": labels,
"series": series
},
options: {
donut: true,
height: 260,
donutWidth: 50,
startAngle: 270,
showLabel: true
}
};
});
}
}
@@ -1,7 +0,0 @@
//export * from './jumps-by-month/jumps-by-month.component';
export * from './drop-zones/drop-zones.component';
//export * from './drop-zones-bar/drop-zones-bar.component';
export * from './aeronefs/aeronefs.component';
//export * from './aeronefs-bar/aeronefs-bar.component';
//export * from './canopy-sizes/canopy-sizes.component';
export * from './canopy-models/canopy-models.component';
@@ -1,54 +0,0 @@
<mat-card>
<mat-card-content class="mb-0">
<div class="d-flex flex-wrap">
<div>
<mat-card-title>Volume mensuel</mat-card-title>
<mat-card-subtitle>Nombre total de sauts par mois de {{min}} à {{max}}</mat-card-subtitle>
</div>
</div>
<div class="barchrt">
<x-chartist class="" [data]="barChartJumps.data" [type]="barChartJumps.type" [options]="barChartJumps.options" [responsiveOptions]="barChartJumps.responsiveOptions"
[events]="barChartJumps.events"> </x-chartist>
</div>
<div class="text-center mt-3 mb-2">
<ul class="list-inline my-0">
<li *ngFor='let name of seriesName; let i=index'>
<h4 class="{{seriesColor[i]}} m-0">
<i class="mdi mdi-checkbox-blank-circle font-10 m-r-10 "></i>{{name}}
</h4>
</li>
</ul>
</div>
</mat-card-content>
<mat-card-content class="pt-0">
<table class="table table-striped table-dark table-hover">
<thead>
<tr>
<th></th>
<th *ngFor='let name of seriesHeader; let i=index' class="text-right"> {{name}} </th>
<th class="text-right">Total</th>
</tr>
</thead>
<tbody>
<tr *ngFor='let values of seriesRow; let i=index'>
<th class="{{seriesColor[i]}} text-right"><i class="mdi mdi-checkbox-blank-circle font-10 m-r-10 "></i> {{seriesName[i]}} </th>
<td *ngFor='let value of values; let i=index' class="text-mono text-right">
<span *ngIf="value; else elseBlock" class="pr-1">{{value}}</span>
<ng-template #elseBlock><span class="text-empty pr-1">{{value}}</span></ng-template>
</td>
<th class="text-mono text-right">{{seriesRowTotal[i]}}</th>
</tr>
</tbody>
<tfoot>
<tr>
<th class="text-right">Total</th>
<th *ngFor='let value of seriesColTotal; let i=index' class="text-mono text-right">
<span *ngIf="value; else elseBlock" class="pr-1">{{value}}</span>
<ng-template #elseBlock><span class="text-empty pr-1">{{value}}</span></ng-template>
</th>
<th class="text-mono text-right">{{grandTotal}}</th>
</tr>
</tfoot>
</table>
</mat-card-content>
</mat-card>
@@ -1,164 +0,0 @@
import { Component, OnInit } from '@angular/core';
import { MatCardModule } from '@angular/material/card';
import { ChartistModule, Configuration } from 'ng-chartist';
import { BackendService } from 'src/app/core/services';
import { ReduceRow } from 'src/app/core/models';
declare var require: any;
const data= require('./data.json');
@Component({
standalone: true,
imports: [MatCardModule, ChartistModule],
selector: 'huapp-jumps-by-month',
templateUrl: './jumps-by-month.component.html'
})
export class JumpsByMonthComponent implements OnInit {
rows: Object[] = [] as Array<Object>;
min: number = 0;
max: number = 0;
grandTotal: number = 0;
seriesHeader: string[] = ["Janvier", "Février", "Mars", "Avril", "Mai", "Juin", "Juillet", "Août", "Septembre", "Octobre", "Novembre", "Décembre"];
seriesRow: any[] = [];
seriesRowTotal: number[] = [];
seriesColTotal: number[] = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
seriesName: String[] = []; // ["2018", "2019", "2020", "2021", "2022", "2023"];
seriesColor: String[] = this._dataService.getChartColors();
barChartJumps: Configuration = {
type: 'Bar',
data: {
"labels": [],
"series": []
},
options: {
seriesBarDistance: 5,
high: 70,
axisX: {
showGrid: false,
offset: 20
},
axisY: {
showGrid: true,
offset: 40
},
height: 300
},
responsiveOptions: [
['screen and (min-width: 1024px)', {
axisX: {
labelInterpolationFnc: function(value: number, index: number): string {
return index % 1 === 0 ? `${value}` : '';
}
}
}],
['screen and (min-width: 641px) and (max-width: 1024px)', {
seriesBarDistance: 10,
axisY: {
offset: 30
},
axisX: {
labelInterpolationFnc: function(value: any, index: number): string {
return index % 1 === 0 ? `${value[0]}` : '';
}
}
}],
['screen and (max-width: 640px)', {
seriesBarDistance: 5,
axisY: {
offset: 20
},
axisX: {
labelInterpolationFnc: function(value: any, index: number): string {
return index % 1 === 0 ? `${value[0]}` : '';
}
}
}]
]
};
constructor(private _dataService: BackendService) { }
ngOnInit() {
return this._dataService
.getJumpsByDate(2)
.subscribe((res) => {
//console.log(res["rows"]);
this.rows = res["rows"]
this.rows.forEach((row: ReduceRow) => {
let year = row.key[0].toString();
let month = row.key[1];
if (this.seriesName.indexOf(year) < 0) {
this.seriesName.push(year);
if (row.key[0] < this.min || this.min == 0) {
this.min = row.key[0];
}
if (row.key[0] > this.max) {
this.max = row.key[0];
}
this.seriesRow.push([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]);
}
this.seriesRow[(this.seriesRow.length-1)][(month-1)] = row.value;
this.seriesColTotal[(month-1)] += row.value;
//console.log(row.key, row.value);
});
this.seriesRow.forEach((row: number[]) => {
const total = row.reduce((partialSum, a) => partialSum + a, 0);
this.seriesRowTotal.push(total);
});
console.log(this.seriesColTotal);
this.grandTotal = this.seriesColTotal.reduce((partialSum, a) => partialSum + a, 0);
//console.log(this.seriesRow, this.seriesName);
this.barChartJumps = {
type: 'Bar',
data: {
"labels": this.seriesHeader,
"series": this.seriesRow
},
options: {
seriesBarDistance: 15,
high: 70,
axisX: {
showGrid: false,
offset: 20
},
axisY: {
showGrid: true,
offset: 40
},
height: 300
},
responsiveOptions: [
['screen and (min-width: 1024px)', {
axisX: {
labelInterpolationFnc: function(value: number, index: number): string {
return index % 1 === 0 ? `${value}` : '';
}
}
}],
['screen and (min-width: 641px) and (max-width: 1024px)', {
seriesBarDistance: 10,
axisY: {
offset: 30
},
axisX: {
labelInterpolationFnc: function(value: any, index: number): string {
return index % 1 === 0 ? `${value[0]}` : '';
}
}
}],
['screen and (max-width: 640px)', {
seriesBarDistance: 5,
axisY: {
offset: 20
},
axisX: {
labelInterpolationFnc: function(value: any, index: number): string {
return index % 1 === 0 ? `${value[0]}` : '';
}
}
}]
]
};
return true;
});
}
}
@@ -1,21 +0,0 @@
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { HomeComponent } from './home.component';
import { AuthResolver } from 'src/app/core/services';
const routes: Routes = [
{
path: '',
component: HomeComponent,
resolve: {
isAuthenticated: AuthResolver
}
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class HomeRoutingModule { }
File diff suppressed because one or more lines are too long
@@ -0,0 +1,17 @@
/* Home */
//@use 'variable' as var;
/*
#FF41F8
#F0060B
#F0070C
#CC26D5
#7702FF
#35A2EB
#B7DDD0
#B0D9CB
#D9CAAE
#9788C7
*/
@@ -6,10 +6,12 @@ describe('HomeComponent', () => {
let component: HomeComponent; let component: HomeComponent;
let fixture: ComponentFixture<HomeComponent>; let fixture: ComponentFixture<HomeComponent>;
beforeEach(() => { beforeEach(async () => {
TestBed.configureTestingModule({ await TestBed.configureTestingModule({
declarations: [HomeComponent] imports: [HomeComponent]
}); })
.compileComponents();
fixture = TestBed.createComponent(HomeComponent); fixture = TestBed.createComponent(HomeComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
fixture.detectChanges(); fixture.detectChanges();
+47 -98
View File
@@ -1,114 +1,63 @@
import { Component, OnInit, OnDestroy } from '@angular/core'; import { DatePipe } from '@angular/common';
import { Title } from '@angular/platform-browser'; import { trigger, state, style, animate, transition } from '@angular/animations';
import { ActivatedRoute, Data, Router } from '@angular/router'; import { Component, DestroyRef, inject, OnInit } from '@angular/core';
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { MatCardModule } from '@angular/material/card';
import { MatDividerModule } from '@angular/material/divider';
import { Observable, Subscription } from 'rxjs';
import { Observable, Observer, Subscription } from 'rxjs'; import { ListErrorsComponent, } from 'src/app/components/shared';
import { take } from 'rxjs/operators'; import { Errors, Jump } from 'src/app/core/models';
import { JumpsService } from 'src/app/core/services';
import { User, Errors, Aeronef, AeronefAggregate, AeronefList, Canopy, CanopyAggregate, DropZone, DropZoneAggregate } from 'src/app/core/models';
import { AeronefsService, CanopiesService, DropZonesService, UserService } from 'src/app/core/services';
import { ShowAuthedDirective } from 'src/app/components/shared/show-authed.directive';
import { AeronefsComponent, DropZonesComponent, CanopyModelsComponent } from './dashboard-components';
//import { JumpsByMonthComponent, AeronefsComponent, AeronefsBarComponent, DropZonesComponent, DropZonesBarComponent, CanopySizesComponent, CanopyModelsComponent} from './dashboard-components';
@Component({ @Component({
selector: 'app-home',
standalone: true, standalone: true,
imports: [ imports: [
//AeronefsComponent, AeronefsBarComponent, DropZonesComponent, DropZonesBarComponent, DatePipe,
//CanopySizesComponent, CanopyModelsComponent, JumpsByMonthComponent, MatCardModule, MatDividerModule,
AeronefsComponent, DropZonesComponent, CanopyModelsComponent, ShowAuthedDirective ListErrorsComponent
], ],
selector: 'huapp-home',
templateUrl: './home.component.html', templateUrl: './home.component.html',
styleUrls: ['./home.component.scss'] styleUrl: './home.component.scss',
animations: [
trigger('flyInOut', [
state('in', style({ transform: 'translateY(0)' })),
transition('void => *', [
style({ transform: 'translateY(-100%)' }),
animate(200)
]),
transition('* => void', [
style({ transform: 'translateY(100%)' }),
animate(200)
])
])
]
}) })
export class HomeComponent implements OnInit, OnDestroy { export class HomeComponent implements OnInit {
private _currentUser: Subscription = new Subscription(); private _lastjump: Subscription = new Subscription();
private _data: Subscription = new Subscription(); private _lastjump$: Observable<Jump> = new Observable();
private _aeronefs: Subscription = new Subscription(); public title = 'Home';
private _canopies: Subscription = new Subscription(); public errors: Errors = { errors: {} };
private _dropzones: Subscription = new Subscription(); public lastJump: Jump = {} as Jump;
title = 'Dashboard'; public destroyRef = inject(DestroyRef);
errors!: Errors; public now = new Date();
isAuthenticated = false; public isJumpDay = false;
canModify = false;
aeronefAggregate!: Array<AeronefAggregate>
aeronefs!: Array<Aeronef>;
aeronefsCount = 0;
canopyAggregate!: Array<CanopyAggregate>
canopies!: Array<Canopy>;
canopiesCount = 0;
dropzoneAggregate!: Array<DropZoneAggregate>
dropzones!: Array<DropZone>;
dropzonesCount = 0;
constructor( constructor(
private route: ActivatedRoute, private _jumpsService: JumpsService
private router: Router, ) { }
private titleService: Title,
private aeronefsService: AeronefsService,
private canopiesService: CanopiesService,
private dropzonesService: DropZonesService,
private userService: UserService
) {
this._resetErrors();
}
ngOnInit() { ngOnInit() {
this.titleService.setTitle(this.title); this._lastjump$ = this._jumpsService.getLastJump().pipe(takeUntilDestroyed(this.destroyRef));
const data$: Observable<any> = this.route.data.pipe(take(1)); this._lastjump = this._lastjump$.subscribe((jump) => {
this._data = data$.subscribe((data: Data) => { this.lastJump = jump;
//data: Partial<Observer<Data>> this.isJumpDay = this._isToday(new Date(this.lastJump.date));
this.isAuthenticated = data['isAuthenticated'];
if (!this.isAuthenticated) {
this.router.navigateByUrl('/login');
return;
}
const currentUser$: Observable<User> = this.userService.currentUser.pipe(take(1));
this._currentUser = currentUser$.subscribe((userData: User) => {
this.canModify = userData.role === 'Admin';
});
});
this.loadAeronefs();
this.loadCanopies();
this.loadDropzones();
}
loadAeronefs() {
const aeronefs$: Observable<Array<AeronefAggregate>> = this.aeronefsService.getAll();
this._aeronefs = aeronefs$.subscribe((aggregate: Array<AeronefAggregate>) => {
this.aeronefAggregate = aggregate;
this.aeronefsCount = aggregate.length;
console.log(this.aeronefs);
console.log(this.aeronefsCount);
}); });
} }
loadCanopies() { private _isToday(someDate: Date) {
const canopies$: Observable<Array<CanopyAggregate>> = this.canopiesService.getAll(); return someDate.getDate() == this.now.getDate() &&
this._canopies = canopies$.subscribe((aggregate: Array<CanopyAggregate>) => { someDate.getMonth() == this.now.getMonth() &&
this.canopyAggregate = aggregate; someDate.getFullYear() == this.now.getFullYear()
this.canopiesCount = aggregate.length;
// console.log(this.canopies);
// console.log(this.canopiesCount);
});
}
loadDropzones() {
const dropzones$: Observable<Array<DropZoneAggregate>> = this.dropzonesService.getAll();
this._dropzones = dropzones$.subscribe((aggregate: Array<DropZoneAggregate>) => {
this.dropzoneAggregate = aggregate;
this.dropzonesCount = aggregate.length;
// console.log(this.dropzones);
// console.log(this.dropzonesCount);
});
}
ngOnDestroy() {
this._aeronefs.unsubscribe();
this._canopies.unsubscribe();
this._dropzones.unsubscribe();
}
private _resetErrors(): void {
this.errors = { errors: {} };
} }
} }
-15
View File
@@ -1,15 +0,0 @@
import { NgModule } from '@angular/core';
import { AuthResolver } from 'src/app/core/services';
import { HomeComponent } from './home.component';
import { HomeRoutingModule } from './home-routing.module';
@NgModule({
imports: [
HomeRoutingModule,
HomeComponent
],
exports: [],
providers: [AuthResolver]
})
export class HomeModule { }
+8
View File
@@ -1,6 +1,14 @@
export * from './auth/auth.component'; export * from './auth/auth.component';
export * from './aeronefs/aeronefs.component';
export * from './calculator/calculator.component'; export * from './calculator/calculator.component';
export * from './canopies/canopies.component';
export * from './dashboard/dashboard.component';
export * from './dropzones/dropzones.component';
export * from './home/home.component'; export * from './home/home.component';
export * from './jump/jump.component';
export * from './jumps/jumps.component';
export * from './logbook/logbook.component'; export * from './logbook/logbook.component';
export * from './profile/profile.component'; export * from './profile/profile.component';
export * from './qcm/qcm.component';
export * from './settings';
export * from './shared'; export * from './shared';
+283
View File
@@ -0,0 +1,283 @@
<div class="d-flex">
<div class="flex-fill">
<h1 class="mb-1">
{{title}}
<span class="fs-3 text-accent ms-2">{{ jump.categorie }} {{ jump.module }}</span>
<span class="fs-3 text-muted ms-2">{{ jump.voile }} {{ jump.taille }}</span>
</h1>
<span class="me-2 fs-6">
Le <span class="fs-5 mx-1 text-primary">{{ jump.date | date: 'dd/MM/yyyy' }}</span>
à <span class="fs-5 mx-1 text-primary">{{ jump.lieu }} {{ jump.oaci }}</span>
@if (jump.x2data) {
depuis <span class="fs-5 mx-1 text-primary">{{ jump.aeronef }} {{ jump.imat }}</span>
}
</span>
</div>
<span class="flex-spacer"></span>
<div class="align-self-center me-4">
<button mat-button [routerLink]="['/jump', prevSlug]" aria-label="Saut précédent" color="accent">
<mat-icon fontIcon="navigate_before"></mat-icon> {{ jump.numero - 1}}
</button>
<div class="vr mx-2 fs-3 align-middle"></div>
<span class="mx-2 align-middle">{{ jump.numero }}</span>
<div class="vr mx-2 fs-3 align-middle"></div>
<button mat-button [routerLink]="['/jump', nextSlug]" aria-label="Saut suivant" color="accent">
<mat-icon fontIcon="navigate_next" iconPositionEnd></mat-icon> {{ jump.numero +1}}
</button>
</div>
<div class="align-self-center me-2">
<button mat-icon-button [matMenuTriggerFor]="menuJump" aria-label="Menu saut">
<mat-icon fontIcon="more_vert"></mat-icon>
</button>
<mat-menu #menuJump="matMenu">
@if (jump.x2data) {
<button mat-menu-item>
<mat-icon fontIcon="language" color="accent"></mat-icon>
<span class="text-accent fw-medium">Obtenir le KML</span>
</button>
<mat-divider></mat-divider>
}
@for (menuitem of menuItems.getMenuJump(); track menuitem) {
<button mat-menu-item>
<mat-icon [fontIcon]="menuitem.icon" [color]="menuitem.color"></mat-icon>
<span class="text-{{menuitem.color}} fw-medium">{{ menuitem.name }}</span>
</button>
}
</mat-menu>
</div>
</div>
<mat-divider class="my-3"></mat-divider>
<mat-card>
<mat-card-content>
<div class="row">
<div class="col-lg-6 col-md-12">
<dl class="dl-horizontal">
<dt class="text-light-info fw-normal">Aeronef:</dt>
<dd>{{ jump.aeronef }} {{ jump.imat }}</dd>
<dt class="text-light-info fw-normal">Voile:</dt>
<dd>{{ jump.voile }} {{ jump.taille }} ft<sup>2</sup></dd>
@if (jump.programme) {
<dt class="text-light-info fw-normal">Programme:</dt>
<dd>{{ jump.programme }}</dd>
}
</dl>
</div>
<div class="col-lg-6 col-md-12">
<dl class="dl-horizontal">
@if (!jump.x2data) {
<dt class="text-light-info fw-normal">Hauteur:</dt>
<dd>{{ jump.hauteur }} m</dd>
<dt class="text-light-info fw-normal">Déploiement:</dt>
<dd>{{ jump.deploiement }} m</dd>
}
@for (file of jump.files; track file) {
<dt class="text-light-info fw-normal">Fichier {{ file.type }}</dt>
<dd>{{ file.name }}</dd>
}
</dl>
</div>
</div>
</mat-card-content>
</mat-card>
@if (jump.x2data) {
<mat-card class="mt-3">
<mat-card-content>
<div class="row my-3">
<div class="col-sm-4 col-xs-12 text-center">
<h6>Hauteur</h6>
@if (jump.x2data.altitude.exit > 4200) {
<mat-icon class="align-middle fs-5 me-2" fontIcon="mood" color="primary"></mat-icon>
}
<span class="text-primary fw-medium fs-5">{{ jump.x2data.altitude.exit }} m</span>
</div>
<div class="col-sm-4 col-xs-12 text-center">
<h6 class="d-flex justify-content-center"><mat-icon fontIcon="timer" class="me-1 fs-5"></mat-icon> Durée de la chute</h6>
<span class="text-primary fw-medium fs-4">{{ formatDuration(jump.x2data.duration.freeFall) }}</span>
</div>
<div class="col-sm-4 col-xs-12 text-center">
<h6>Déploiement</h6>
@if (jump.x2data.altitude.deployment < 650) {
<mat-icon class="align-middle fs-5 me-2" fontIcon="warning" color="raspberry"></mat-icon>
<span class="text-raspberry fw-medium fs-5">{{ jump.x2data.altitude.deployment }} m</span>
}
@else if (jump.x2data.altitude.deployment < 750) {
<mat-icon class="align-middle fs-5 me-2" fontIcon="local_florist" color="warn"></mat-icon>
<span class="text-warn fw-medium fs-5">{{ jump.x2data.altitude.deployment }} m</span>
}
@else if (jump.x2data.altitude.deployment > 1000) {
<mat-icon class="align-middle fs-5 me-2" fontIcon="mood" color="primary"></mat-icon>
<span class="text-primary fw-medium fs-5">{{ jump.x2data.altitude.deployment }} m</span>
}
</div>
</div>
<div class="row mt-4">
<div class="{{ jump.accessoires ? 'col-sm-6 ' : '' }}col-xs-12 text-center">
<h5 class="d-flex justify-content-center mb-4"><mat-icon fontIcon="people" class="me-2"></mat-icon> Participants ({{ jump.sautants.length + 1 }})</h5>
@for (sautant of jump.sautants; track sautant) {
<span class="badge rounded-pill bg-primary mb-2 me-1 pe-3 py-1 fw-normal text-dark">
<mat-icon class="align-middle me-1" fontIcon="person"></mat-icon>
{{ sautant }}
</span>
}
<span class="badge rounded-pill bg-accent mb-2 me-1 pe-3 py-1 fw-normal text-dark">
<mat-icon class="align-middle me-1" fontIcon="person"></mat-icon>
{{ jump.author.username }}
</span>
</div>
@if (jump.accessoires) {
<div class="col-sm-6 col-xs-12 text-center">
<h5 class="d-flex justify-content-center mb-4"><mat-icon fontIcon="auto_awesome" class="me-2"></mat-icon> Accessoires</h5>
<span class="badge rounded-pill bg-navy-light mb-2 me-1 pe-3 py-1 fw-normal">
<mat-icon class="align-middle me-1" fontIcon="auto_awesome"></mat-icon>
{{ jump.accessoires }}
</span>
</div>
}
</div>
</mat-card-content>
</mat-card>
<mat-card class="mt-3">
<mat-card-content>
<div class="row my-3">
<div class="col-xs-12 text-center">
<h5 class="d-flex justify-content-center mb-3"><mat-icon fontIcon="speed" class="me-1"></mat-icon> Vitesse en chute</h5>
</div>
<div class="col-sm-6 hidden-xs text-center">
<h6>Verticale</h6>
</div>
<div class="col-sm-6 hidden-xs text-center">
<h6>Horizontale</h6>
</div>
<div class="col-sm-3 col-xs-12 text-center">
<h6>Vitesse maximale</h6>
<span class="text-primary fw-medium fs-5">{{ (jump.x2data.speed.freeFall.vertical.max / 1000 * 60 * 60) | number : '1.0-0' }} km/h</span>
</div>
<div class="col-sm-3 col-xs-12 text-center">
<h6>Vitesse moyenne</h6>
<span class="text-primary fw-medium fs-5">{{ (jump.x2data.speed.freeFall.vertical.avg / 1000 * 60 * 60) | number : '1.0-0' }} km/h</span>
</div>
<div class="col-sm-3 col-xs-12 text-center">
<h6>Vitesse maximale</h6>
<span class="text-primary fw-medium fs-5">{{ (jump.x2data.speed.freeFall.horizontal.max / 1000 * 60 * 60) | number : '1.0-0' }} km/h</span>
</div>
<div class="col-sm-3 col-xs-12 text-center">
<h6>Vitesse moyenne</h6>
<span class="text-primary fw-medium fs-5">{{ (jump.x2data.speed.freeFall.horizontal.avg / 1000 * 60 * 60) | number : '1.0-0' }} km/h</span>
</div>
</div>
</mat-card-content>
</mat-card>
<mat-card class="mt-3">
<mat-card-content>
<div class="row my-3">
<div class="col-xs-12 text-center">
<h5 class="d-flex justify-content-center mb-3"><mat-icon fontIcon="speed" class="me-1"></mat-icon> Vitesse sous voile</h5>
</div>
<div class="col-sm-6 hidden-xs text-center">
<h6>Verticale</h6>
</div>
<div class="col-sm-6 hidden-xs text-center">
<h6>Horizontale</h6>
</div>
<div class="col-sm-3 col-xs-12 text-center">
<h6>Vitesse maximale</h6>
<span class="text-primary fw-medium fs-5">{{ (jump.x2data.speed.underCanopy.vertical.max / 1000 * 60 * 60) | number : '1.0-0' }} km/h</span>
</div>
<div class="col-sm-3 col-xs-12 text-center">
<h6>Vitesse moyenne</h6>
<span class="text-primary fw-medium fs-5">{{ (jump.x2data.speed.underCanopy.vertical.avg / 1000 * 60 * 60) | number : '1.0-0' }} km/h</span>
</div>
<div class="col-sm-3 col-xs-12 text-center">
<h6>Vitesse maximale</h6>
<span class="text-primary fw-medium fs-5">{{ (jump.x2data.speed.underCanopy.horizontal.max / 1000 * 60 * 60) | number : '1.0-0' }} km/h</span>
</div>
<div class="col-sm-3 col-xs-12 text-center">
<h6>Vitesse moyenne</h6>
<span class="text-primary fw-medium fs-5">{{ (jump.x2data.speed.underCanopy.horizontal.avg / 1000 * 60 * 60) | number : '1.0-0' }} km/h</span>
</div>
</div>
</mat-card-content>
</mat-card>
<mat-card class="mt-3">
<mat-card-content>
<div class="row my-3">
<div class="col-xs-12 text-center">
<h5 class="d-flex justify-content-center mb-3"><mat-icon fontIcon="square_foot" class="me-1"></mat-icon> Finesse</h5>
</div>
<div class="col-sm-6 hidden-xs text-center">
<h6>En chute</h6>
</div>
<div class="col-sm-6 hidden-xs text-center">
<h6>Sous voile</h6>
</div>
<div class="col-sm-3 col-xs-12 text-center">
<h6>Finesse Max</h6>
<span class="text-primary fw-medium fs-5">{{ jump.x2data.glideRatio.freeFall.max | number : '1.2-2' }}</span>
</div>
<div class="col-sm-3 col-xs-12 text-center">
<h6>Finesse Moyenne</h6>
<span class="text-primary fw-medium fs-5">{{ jump.x2data.glideRatio.freeFall.avg | number : '1.2-2' }}</span>
</div>
<div class="col-sm-3 col-xs-12 text-center">
<h6>Finesse Max</h6>
<span class="text-primary fw-medium fs-5">{{ jump.x2data.glideRatio.underCanopy.max | number : '1.2-2' }}</span>
</div>
<div class="col-sm-3 col-xs-12 text-center">
<h6>Finesse Moyenne</h6>
<span class="text-primary fw-medium fs-5">{{ jump.x2data.glideRatio.underCanopy.avg | number : '1.2-2' }}</span>
</div>
</div>
</mat-card-content>
</mat-card>
<mat-card class="mt-3">
<mat-card-content>
<div class="row my-3">
<div class="col-xs-12 text-center">
<h5 class="d-flex justify-content-center mb-3"><mat-icon fontIcon="straighten" class="me-1"></mat-icon> Distance</h5>
</div>
<div class="col-sm-3 col-xs-12 text-center">
<h6>Distance au largage</h6>
<span class="text-primary fw-medium fs-5">{{ jump.x2data.distance.exitFromLandingZone| number : '1.0-0' }} m</span>
</div>
<div class="col-sm-3 col-xs-12 text-center">
<h6>Durée sous voile</h6>
<span class="text-primary fw-medium fs-5">{{ formatDuration(jump.x2data.duration.underCanopy) }}</span>
</div>
<div class="col-sm-3 col-xs-12 text-center">
<h6>Distance totale</h6>
<span class="text-primary fw-medium fs-5">{{ jump.x2data.distance.totalFlying | number : '1.0-0' }} m</span>
</div>
<div class="col-sm-3 col-xs-12 text-center">
<h6>Distance horizontale</h6>
<span class="text-primary fw-medium fs-5">{{ jump.x2data.distance.totalHorizontal | number : '1.0-0' }} m</span>
</div>
</div>
</mat-card-content>
</mat-card>
}
@else {
<mat-card class="mt-3">
<mat-card-content>
<div class="row my-3">
<div class="col-xs-12 text-center">
<h5 class="d-flex justify-content-center mb-4"><mat-icon fontIcon="people" class="me-1"></mat-icon> Participants ({{ jump.sautants.length + 1 }})</h5>
@for (sautant of jump.sautants; track sautant) {
<span class="badge rounded-pill bg-primary mb-2 me-1 pe-3 py-1 fw-normal text-dark">
<mat-icon class="align-middle me-1" fontIcon="person"></mat-icon>
{{ sautant }}
</span>
}
<span class="badge rounded-pill bg-accent mb-2 me-1 pe-3 py-1 fw-normal text-dark">
<mat-icon class="align-middle me-1" fontIcon="person"></mat-icon>
{{ jump.author.username }}
</span>
</div>
</div>
</mat-card-content>
</mat-card>
<mat-card class="mt-3">
<mat-card-content>
<div class="d-flex justify-content-center my-3 fs-5"><mat-icon fontIcon="satellite_alt" class="me-2"></mat-icon> Aucune donnée GPS</div>
</mat-card-content>
</mat-card>
}
@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { JumpComponent } from './jump.component';
describe('JumpComponent', () => {
let component: JumpComponent;
let fixture: ComponentFixture<JumpComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [JumpComponent]
})
.compileComponents();
fixture = TestBed.createComponent(JumpComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
+71
View File
@@ -0,0 +1,71 @@
import { Component, DestroyRef, inject, OnInit, OnDestroy } from '@angular/core';
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { DatePipe, DecimalPipe } from '@angular/common';
import { ActivatedRoute, RouterLink } from '@angular/router';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatDividerModule } from '@angular/material/divider';
import { MatIconModule } from '@angular/material/icon';
import { MatMenuModule } from '@angular/material/menu';
import { Observable, Subscription } from 'rxjs';
import { ListErrorsComponent, MenuItems } from 'src/app/components/shared';
import { Errors, Jump, JumpPageData } from 'src/app/core/models';
@Component({
selector: 'app-jump',
standalone: true,
imports: [
DatePipe, DecimalPipe, RouterLink,
MatButtonModule, MatCardModule,
MatDividerModule, MatIconModule, MatMenuModule,
ListErrorsComponent
],
templateUrl: './jump.component.html',
styleUrl: './jump.component.scss'
})
export class JumpComponent implements OnInit, OnDestroy {
private _data: Subscription = new Subscription();
public title: string = '';
public errors: Errors = { errors: {} };
public destroyRef = inject(DestroyRef);
public jump: Jump = {} as Jump;
public prevSlug: string = '';
public nextSlug: string = '';
constructor(
private route: ActivatedRoute,
public menuItems: MenuItems
) { }
ngOnInit() {
const data$: Observable<{ pageData: JumpPageData }> = this.route.data as Observable<{ pageData: JumpPageData }>;
this._data = data$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((data: { pageData: JumpPageData }) => {
this.jump = data.pageData.jump;
this.prevSlug = data.pageData.prevJump?.slug;
this.nextSlug = data.pageData.nextJump?.slug;
this.title = `Saut n° ${this.jump.numero} `;
});
/*
const data$: Observable<Jump> = this.route.data as Observable<Jump>;
this._data = data$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((data: Jump) => {
this.jump = data;
console.log('jump : ', this.jump);
});
*/
}
ngOnDestroy() {
this._data.unsubscribe();
}
formatDuration(duration: number): string {
const start = new Date(0);
start.setSeconds(duration);
return start.toISOString().substring(14, 19);
}
private _resetErrors(): void {
this.errors = { errors: {} };
}
}
@@ -0,0 +1,130 @@
<div @flyInOut>
<div class="d-flex">
<div class="flex-fill">
<h1 class="mb-1">{{title}}</h1>
<span [hidden]="!lastJump.numero" class="me-2 fs-6">
Dernier saut enregistré : <span class="fs-5 mx-1 text-primary">{{ lastJump.numero }}<sup>ème</sup></span> à <span class="fs-5 mx-1 text-primary">{{ lastJump.lieu }}</span> le <span class="fs-5 mx-1 text-primary">{{ lastJump.date | date: 'dd/MM/yyyy' }}</span>
</span>
</div>
<span class="flex-spacer"></span>
<div class="align-self-center me-2">
<button mat-icon-button [matMenuTriggerFor]="menuJumps" aria-label="Menu sauts">
<mat-icon fontIcon="more_vert"></mat-icon>
</button>
<mat-menu #menuJumps="matMenu">
@for (menuitem of menuItems.getMenuPanel(); track menuitem) {
<button mat-menu-item>
<mat-icon [fontIcon]="menuitem.icon"></mat-icon>
<span>{{ menuitem.name }}</span>
</button>
}
</mat-menu>
</div>
</div>
<mat-divider class="my-3"></mat-divider>
<mat-card>
<mat-card-header>
<mat-card-title>Types de sauts</mat-card-title>
</mat-card-header>
<mat-card-content class="mt-2">
@if(displayCharts) {
<div class="row">
<div class="col-xs-12">
<app-line-chart [headers]="seriesTypesHeader" [names]="seriesTypesName" [values]="seriesTypesRowCumulated" [colors]="seriesTypesColor" [legend]="true" [hideZero]="true"></app-line-chart>
</div>
</div>
<mat-divider class="mt-2 mb-3"></mat-divider>
<div class="row">
@for (item of getCategories(); track $index) {
<div class="col-lg-2 col-sm-4 col-xs-6">
<app-circle-chart [headers]="seriesHeader" [names]="seriesName" [values]="[item.count, (lastJump.numero-item.count)]" [color]="$index" [label]="item.categorie"></app-circle-chart>
</div>
}
@for (item of getModules(); track $index) {
<div class="col-lg-2 col-sm-4 col-xs-6">
<app-circle-chart [headers]="seriesHeader" [names]="seriesName" [values]="[item.count, (lastJump.numero-item.count)]" [color]="getModuleColor($index)" [label]="item.module"></app-circle-chart>
</div>
}
</div>
<mat-divider class="my-3"></mat-divider>
<!--
<div class="row">
<div class="col-xxl-6 col-xs-12">
<app-line-chart [headers]="seriesTypesHeader" [names]="seriesTypesName" [values]="seriesTypesRow" [colors]="seriesTypesColor" [legend]="true"></app-line-chart>
</div>
<div class="col-xxl-6 col-xs-12">
<app-line-chart [headers]="seriesTypesHeader" [names]="seriesTypesName" [values]="seriesTypesRowCumulated" [colors]="seriesTypesColor" [legend]="true"></app-line-chart>
</div>
</div>
-->
}
</mat-card-content>
</mat-card>
<mat-card class="mt-3">
<mat-card-header>
<mat-card-title>{{ subtitle }}</mat-card-title>
</mat-card-header>
<mat-card-content class="mt-2">
@if(displayCharts) {
<div class="row">
<div class="col-xxl-6 col-xs-12">
<app-linearea-chart [headers]="seriesHeader" [names]="seriesName" [values]="seriesRow" [colors]="seriesColor"></app-linearea-chart>
</div>
<div class="col-xxl-6 col-xs-12">
<app-bars-chart [headers]="seriesHeader" [names]="seriesName" [values]="seriesRow" [colors]="seriesColor" [legend]="true"></app-bars-chart>
</div>
</div>
}
<div class="row">
<div class="col-xs-12">
<mat-divider class="mt-3 mb-0"></mat-divider>
<table class="table table-striped table-dark table-hover">
<thead>
<tr>
<th></th>
<th *ngFor='let name of seriesHeader; let i=index' class="text-end"> {{ name }} </th>
<th class="text-end">Total</th>
</tr>
</thead>
<tbody>
<tr *ngFor='let values of seriesRow; let i=index'>
<th class="{{seriesColorClass[i]}} text-end"> ● {{ seriesName[i] }} </th>
<td *ngFor='let value of values; let i=index' class="font-monospace text-end">
<span *ngIf="value; else elseBlock" class="pr-1">{{ value }}</span>
<ng-template #elseBlock><span class="text-empty pr-1">{{ value }}</span></ng-template>
</td>
<th class="{{seriesColorClass[i]}} font-monospace text-end">{{ seriesRowTotal[i] }}</th>
</tr>
</tbody>
<tfoot>
<tr>
<th class="text-end">Total</th>
<th *ngFor='let value of seriesColTotal; let i=index' class="font-monospace text-end">
<span *ngIf="value; else elseBlock" class="pr-1">{{ value }}</span>
<ng-template #elseBlock><span class="text-empty pr-1">{{ value }}</span></ng-template>
</th>
<th class="font-monospace text-end">{{ grandTotal }}</th>
</tr>
<tr>
<th class="text-end">Moyenne</th>
<th *ngFor='let value of seriesRowAvg; let i=index' class="font-monospace text-end">
<span *ngIf="value; else elseBlock" class="pr-1">{{ value | number : '1.0-1' }}</span>
<ng-template #elseBlock><span class="text-empty pr-1">{{ value | number : '1.0-1' }}</span></ng-template>
</th>
<th class="font-monospace text-end">{{ grandAvg | number : '1.0-1' }}</th>
</tr>
<tr>
<th class="text-end">Moyenne <small>3 dernières années</small></th>
<th *ngFor='let value of seriesRowAvgLastYears; let i=index' class="font-monospace text-end">
<span *ngIf="value; else elseBlock" class="pr-1">{{ value | number : '1.0-1' }}</span>
<ng-template #elseBlock><span class="text-empty pr-1">{{ value | number : '1.0-1' }}</span></ng-template>
</th>
<th class="font-monospace text-end">{{ grandAvgLastYears | number : '1.0-1' }}</th>
</tr>
</tfoot>
</table>
</div>
</div>
</mat-card-content>
</mat-card>
</div>
@@ -0,0 +1,21 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { JumpsComponent } from './jumps.component';
describe('JumpsComponent', () => {
let component: JumpsComponent;
let fixture: ComponentFixture<JumpsComponent>;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [JumpsComponent]
});
fixture = TestBed.createComponent(JumpsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
+249
View File
@@ -0,0 +1,249 @@
import { trigger, state, style, animate, transition } from '@angular/animations';
import { Component, DestroyRef, inject, OnInit, OnDestroy } from '@angular/core';
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { ActivatedRoute } from '@angular/router';
import { CommonModule } from '@angular/common';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatDividerModule } from '@angular/material/divider';
import { MatExpansionModule } from '@angular/material/expansion';
import { MatIconModule } from '@angular/material/icon';
import { MatMenuModule } from '@angular/material/menu';
import { Observable, Subscription } from 'rxjs';
import { MenuItems } from 'src/app/components/shared';
import { BarsChartComponent, CircleChartComponent, LineChartComponent, LineAreaChartComponent } from 'src/app/components/shared/helpers-chart';
import { UtilitiesService } from 'src/app/core/services';
import { Jump, JumpByCategorie, JumpByDate, JumpByDateByModule, JumpByModule, JumpsPageData, JumpYears } from 'src/app/core/models';
@Component({
selector: 'app-jumps',
standalone: true,
imports: [
CommonModule,
MatButtonModule, MatCardModule, MatDividerModule,
MatExpansionModule, MatIconModule, MatMenuModule,
BarsChartComponent, CircleChartComponent, LineChartComponent, LineAreaChartComponent
],
templateUrl: './jumps.component.html',
styleUrl: './jumps.component.scss',
animations: [
trigger('flyInOut', [
state('in', style({ transform: 'translateX(0)' })),
transition('void => *', [
style({ transform: 'translateX(-100%)' }),
animate(200)
]),
transition('* => void', [
style({ transform: 'translateX(100%)' }),
animate(200)
])
])
]
})
export class JumpsComponent implements OnInit, OnDestroy {
private _data: Subscription = new Subscription();
private _jumpsByCategorie: Array<JumpByCategorie> = [];
private _jumpsByModule: Array<JumpByModule> = [];
private _jumpsByDate: Array<JumpByDate> = [];
public lastJump: Jump = {} as Jump;
public title: string = 'Sauts';
public subtitle: string = 'Nombre total de sauts par mois';
public displayCharts = false;
public destroyRef = inject(DestroyRef);
public min = 0;
public max = 0;
public grandAvg = 0;
public grandAvgLastYears = 0;
public grandTotal = 0;
public seriesHeader: string[] = [];
public seriesName: string[] = [];
public seriesRow: Array<Array<number>> = [];
public seriesColor: {
backgroundColor: string[],
borderColor: string[]
} = {
backgroundColor: this._utilitiesService.getSeriesColors(0.8, 'all'),
borderColor: this._utilitiesService.getSeriesColors(1, 'all')
};
public seriesTypesHeader: string[] = [];
public seriesTypesName: string[] = [];
public seriesTypesRow: Array<Array<number>> = [];
public seriesTypesRowCumulated: Array<Array<number>> = [];
public seriesTypesColor: {
backgroundColor: string[],
borderColor: string[]
} = {
backgroundColor: this._utilitiesService.getSeriesColors(0.8, 'pastels'),
borderColor: this._utilitiesService.getSeriesColors(1, 'pastels')
};
public seriesColorClass: string[] = this._utilitiesService.getChartColors();
public seriesRowTotal: number[] = [];
public seriesRowAvg: number[] = [];
public seriesRowAvgLastYears: number[] = [];
public seriesColTotal: number[] = [];
public seriesColTotalClosed: number[] = [];
public seriesColTotalLastYears: number[] = [];
constructor(
private route: ActivatedRoute,
private _utilitiesService: UtilitiesService,
public menuItems: MenuItems
) { }
ngOnInit() {
const data$: Observable<{ pageData: JumpsPageData }> = this.route.data as Observable<{ pageData: JumpsPageData }>;
this._data = data$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((data: { pageData: JumpsPageData }) => {
const pageData: JumpsPageData = data.pageData;
this.lastJump = pageData.lastjump;
const currentYear: number = new Date().getFullYear();
const types: Array<string> = ['FF', 'Solo', 'VR', 'Wingsuit'];
const verticals: Array<string> = ['Chutas', 'Head Up', 'Head Down'];
const others: Array<string> = ['Track', 'Trace'];
const modules: Array<string> = [...verticals, ...others];
const minoration = 2; // -2 pour ne pas compter la première année
this.min = pageData.jumpsByYears[0].year;
this.max = pageData.jumpsByYears[(pageData.jumpsByYears.length-1)].year;
this.seriesHeader = this._utilitiesService.getMonthsList();
const values: Array<number> = this.seriesHeader.map(() => 0);
this.seriesColTotal = [...values];
this.seriesColTotalClosed = [...values];
this.seriesColTotalLastYears = [...values];
this.seriesName = pageData.jumpsByYears.map((row: JumpYears) => {
this.seriesRow.push([...values]);
return row.year.toString();
});
this._jumpsByDate = pageData.jumpsByDate.map((row: JumpByDate) => {
this.seriesRow[(row.year-this.min)][(row.month-1)] = row.count;
this.seriesColTotal[(row.month-1)] += row.count;
if (row.year < currentYear) {
this.seriesColTotalClosed[(row.month-1)] += row.count;
}
if (row.year >= (currentYear-3) && row.year < currentYear) {
this.seriesColTotalLastYears[(row.month-1)] += row.count;
}
return row;
});
this.seriesRow.forEach((row: number[]) => {
const total = row.reduce((partialSum, a) => partialSum + a, 0);
this.seriesRowTotal.push(total);
});
this.seriesColTotalClosed.forEach((row: number) => {
const value: number = (row/(this.seriesName.length-minoration));
this.seriesRowAvg.push(parseInt(value.toFixed(0)));
});
this.seriesColTotalLastYears.forEach((row: number) => {
const value: number = (row/3); // Les 3 dernières années
this.seriesRowAvgLastYears.push(parseInt(value.toFixed(0)));
});
this.grandAvg = this.seriesRowAvg.reduce((partialSum, accumulated) => partialSum + accumulated, 0);
this.grandAvgLastYears = this.seriesRowAvgLastYears.reduce((partialSum, accumulated) => partialSum + accumulated, 0);
this.grandTotal = this.seriesColTotal.reduce((partialSum, accumulated) => partialSum + accumulated, 0);
pageData.jumpsByCategory.forEach((row: JumpByCategorie) => {
if (types.indexOf(row.categorie) !== -1) {
this._jumpsByCategorie.push(row);
}
});
//const focus: Array<string> = ['FF', 'Solo']; // ['FF']
let verticalCount:number = 0;
let otherCount:number = 0;
pageData.jumpsByModule.forEach((row: JumpByModule) => {
//if (focus.indexOf(row.categorie) !== -1 && modules.indexOf(row.module) !== -1) {
if (modules.indexOf(row.module) !== -1) {
if (verticals.indexOf(row.module) !== -1) {
verticalCount += row.count;
} else {
otherCount += row.count;
}
}
});
if (verticalCount > 0) {
const row:JumpByModule = {
categorie: 'FF',
module: 'Vertical',
count: verticalCount
}
this._jumpsByModule.push(row);
}
if (otherCount > 0) {
const row:JumpByModule = {
categorie: 'FF',
module: 'Track/Trace',
count: otherCount
}
this._jumpsByModule.push(row);
}
for (let year = 0; year < pageData.jumpsByYears.length; year++) {
for (let index = 0; index < 12; index++) {
const month: string = ((index+1) < 10) ? `0${(index+1)}` : `${(index+1)}`;
this.seriesTypesHeader.push(`${month}-${pageData.jumpsByYears[year].year}`);
}
}
const emptyValues: Array<number> = this.seriesTypesHeader.map(() => 0);
this.seriesTypesName = types;
this.seriesTypesName.forEach(() => {
this.seriesTypesRow.push([...emptyValues]);
});
pageData.jumpsByDateByModule.forEach((row: JumpByDateByModule) => {
const indexOf: number = this.seriesTypesName.indexOf(row.categorie);
if (indexOf !== -1) {
this.seriesTypesRow[indexOf][(((row.year-this.min)*12)+row.month-1)] += row.count;
}
});
this.seriesTypesRowCumulated = this.seriesTypesRow.map((row: Array<number>) => {
let accumulated: number = 0;
return row.map((value: number) => {
accumulated += value;
return accumulated;
});
});
this.seriesTypesHeader = this.seriesTypesHeader.slice((pageData.jumpsByDateByModule[0].month+1), -(pageData.jumpsByDateByModule[(pageData.jumpsByDateByModule.length-1)].month));
this.seriesTypesRow = this.seriesTypesRow.map((row: Array<number>) => {
return row.slice((pageData.jumpsByDateByModule[0].month+1), -(pageData.jumpsByDateByModule[(pageData.jumpsByDateByModule.length-1)].month));
});
this.seriesTypesRowCumulated = this.seriesTypesRowCumulated.map((row: Array<number>) => {
return row.slice((pageData.jumpsByDateByModule[0].month+1), -(pageData.jumpsByDateByModule[(pageData.jumpsByDateByModule.length-1)].month));
});
/*
console.log('this.min', this.min);
console.log('this.max', this.max);
console.log('this.lastJump', this.lastJump);
console.log('this.seriesHeader', this.seriesHeader);
console.log('this.seriesName', this.seriesName);
console.log('this.seriesRow', this.seriesRow);
console.log('this.seriesRowTotal', this.seriesRowTotal);
console.log('this.seriesRowAvg', this.seriesRowAvg);
console.log('this.seriesRowAvgLastYears', this.seriesRowAvgLastYears);
console.log('this._jumpsByCategorie', this._jumpsByCategorie);
console.log('this._jumpsByModule', this._jumpsByModule);
console.log('this.seriesTypesHeader', this.seriesTypesHeader);
console.log('this.seriesTypesName', this.seriesTypesName);
console.log('this.seriesTypesRow', this.seriesTypesRow);
console.log('this.seriesTypesRowCumulated', this.seriesTypesRowCumulated);
*/
this.displayCharts = true;
});
}
ngOnDestroy() {
this._data.unsubscribe();
}
public getCategories(): Array<JumpByCategorie> {
return this._jumpsByCategorie;
}
public getModules(): Array<JumpByModule> {
return this._jumpsByModule;
}
public getModuleColor(index: number): number {
return (this._jumpsByCategorie.length + index);
}
}
@@ -1,3 +1,4 @@
export * from './jump-add.dialog'; export * from './jump-add.dialog';
export * from './jump-delete.dialog'; export * from './jump-delete.dialog';
export * from './jump-edit.dialog'; export * from './jump-edit.dialog';
export * from './jump-view.dialog';
@@ -0,0 +1,291 @@
<h2 mat-dialog-title class="bg-warn border-bottom border-light-subtle d-flex mb-3 pt-3 pb-2">
Ajouter un saut<span class="flex-spacer"></span>
<mat-icon fontIcon="close" class="pointer" (click)="closeDialog()"></mat-icon>
</h2>
<mat-dialog-content>
<div class="container">
<!--
<div class="row">
<div class="col-xs-12 border-bottom border-dark pb-2 mb-2">
<mat-form-field appearance="outline" class="mt-2 me-2" color="warn">
<mat-label>Numéro ou plage de saut à ajouter</mat-label>
<input matInput [(ngModel)]="params.numeros" required />
<mat-hint>645 / +5 / 650-660</mat-hint>
</mat-form-field>
<mat-form-field appearance="outline" class="mt-2 me-3" color="warn">
<mat-label>Choisissez une date</mat-label>
<input matInput [matDatepicker]="datepicker" [(ngModel)]="params.date" (dateChange)="onDateChange()" required />
<mat-hint>MM/DD/YYYY</mat-hint>
<mat-datepicker-toggle matIconSuffix [for]="datepicker"></mat-datepicker-toggle>
<mat-datepicker #datepicker>
<mat-datepicker-actions>
<button mat-button matDatepickerCancel>Annuler</button>
<button mat-raised-button color="warn" matDatepickerApply>Appliquer</button>
</mat-datepicker-actions>
</mat-datepicker>
</mat-form-field>
<mat-checkbox (change)="setLastJump($event.checked)" [checked]="useLast.all">
<mat-icon fontIcon="history" class="align-middle me-2"></mat-icon> <span class="d-inline-block align-middle">Utiliser les valeurs du dernier saut</span>
</mat-checkbox>
</div>
</div>
-->
<form class="row" [formGroup]="jumpForm" (ngSubmit)="submitForm()">
<div class="col-xs-12 border-bottom border-dark pb-2 mb-2">
<mat-form-field appearance="outline" class="mt-2 me-2" color="warn">
<mat-label>Numéro ou plage de saut à ajouter</mat-label>
<input matInput formControlName="numero" required />
<mat-hint>645 / +5 / 650-660</mat-hint>
</mat-form-field>
<mat-form-field appearance="outline" class="mt-2 me-3" color="warn">
<mat-label>Choisissez une date</mat-label>
<input matInput [matDatepicker]="datepicker" formControlName="date" (dateChange)="onDateChange()" required cdkFocusInitial />
<mat-hint>MM/DD/YYYY</mat-hint>
<mat-datepicker-toggle matIconSuffix [for]="datepicker"></mat-datepicker-toggle>
<mat-datepicker #datepicker>
<mat-datepicker-actions>
<button mat-button matDatepickerCancel>Annuler</button>
<button mat-raised-button color="warn" matDatepickerApply>Appliquer</button>
</mat-datepicker-actions>
</mat-datepicker>
</mat-form-field>
<mat-checkbox (change)="setLastJump($event.checked)" [checked]="useLast.all">
<mat-icon fontIcon="history" class="align-middle me-2"></mat-icon> <span class="d-inline-block align-middle">Utiliser les valeurs du dernier saut</span>
</mat-checkbox>
</div>
<div class="col-lg-6 col-md-12">
<h4 class="border-bottom border-secondary mb-3 lh-lg">
Dropzone
<mat-checkbox class="float-end" (change)="setLastDropzone($event.checked)" [checked]="useLast.dropzone">
<mat-icon fontIcon="history" class="align-middle"></mat-icon>
</mat-checkbox>
</h4>
<mat-form-field appearance="fill" class="me-2" color="warn">
<mat-label>Lieu</mat-label>
<input matInput formControlName="lieu" [matAutocomplete]="autoDropzone">
<mat-autocomplete #autoDropzone="matAutocomplete" [displayWith]="displayLieuFn">
@for (option of filteredOptions.dropzones | async; track option) {
<mat-option [value]="option" (onSelectionChange)="onSelLieu(option)">{{option.lieu}} - {{option.oaci}}</mat-option>
}
</mat-autocomplete>
@if (this.jumpForm.controls['lieu'].status === 'INVALID') {
<mat-error>{{getErrorMessage('lieu')}}</mat-error>
}
</mat-form-field>
<mat-form-field appearance="fill" class="me-2" color="warn">
<mat-label>Oaci</mat-label>
<input matInput formControlName="oaci" [matAutocomplete]="autoOaci">
<mat-autocomplete #autoOaci="matAutocomplete" [displayWith]="displayOaciFn">
@for (option of filteredOptions.oaci | async; track option) {
<mat-option [value]="option" (onSelectionChange)="onSelOaci(option)">{{option.lieu}} - {{option.oaci}}</mat-option>
}
</mat-autocomplete>
@if (this.jumpForm.controls['oaci'].status === 'INVALID') {
<mat-error>{{getErrorMessage('oaci')}}</mat-error>
}
</mat-form-field>
<h4 class="border-bottom border-secondary mb-3 lh-lg">
Aéronef
<mat-checkbox class="float-end" (change)="setLastAeronef($event.checked)" [checked]="useLast.aeronef">
<mat-icon fontIcon="history" class="align-middle"></mat-icon>
</mat-checkbox>
</h4>
<mat-form-field appearance="fill" class="me-2" color="warn">
<mat-label>Aéronef</mat-label>
<input matInput formControlName="aeronef" [matAutocomplete]="autoAeronef">
<mat-autocomplete #autoAeronef="matAutocomplete" [displayWith]="displayAeronefFn">
@for (option of filteredOptions.aeronefs | async; track option) {
<mat-option [value]="option" (onSelectionChange)="onSelAeronef(option)">{{option.aeronef}} - {{option.imat}}</mat-option>
}
</mat-autocomplete>
@if (this.jumpForm.controls['aeronef'].status === 'INVALID') {
<mat-error>{{getErrorMessage('aeronef')}}</mat-error>
}
</mat-form-field>
<mat-form-field appearance="fill" class="me-2" color="warn">
<mat-label>Imatriculation</mat-label>
<input matInput formControlName="imat" [matAutocomplete]="autoImat">
<mat-autocomplete #autoImat="matAutocomplete" [displayWith]="displayImatFn">
@for (option of filteredOptions.imats | async; track option) {
<mat-option [value]="option" (onSelectionChange)="onSelImat(option)">{{option.aeronef}} - {{option.imat}}</mat-option>
}
</mat-autocomplete>
@if (this.jumpForm.controls['imat'].status === 'INVALID') {
<mat-error>{{getErrorMessage('imat')}}</mat-error>
}
</mat-form-field>
<h4 class="border-bottom border-secondary mb-3 lh-lg">
Voile
<mat-checkbox class="float-end" (change)="setLastVoile($event.checked)" [checked]="useLast.voile">
<mat-icon fontIcon="history" class="align-middle"></mat-icon>
</mat-checkbox>
</h4>
<mat-form-field appearance="fill" class="me-2" color="warn">
<mat-label>Modèle</mat-label>
<input matInput formControlName="voile" [matAutocomplete]="autoCanopy">
<mat-autocomplete #autoCanopy="matAutocomplete" [displayWith]="displayVoileFn">
@for (option of filteredOptions.canopies | async; track option) {
<mat-option [value]="option" (onSelectionChange)="onSelVoile(option)">{{option.voile}} - {{option.taille}}</mat-option>
}
</mat-autocomplete>
@if (this.jumpForm.controls['voile'].status === 'INVALID') {
<mat-error>{{getErrorMessage('voile')}}</mat-error>
}
</mat-form-field>
<mat-form-field appearance="fill" class="me-2" color="warn">
<mat-label>Taille en ft<sup>2</sup></mat-label>
<input matInput formControlName="taille" [matAutocomplete]="autoTaille">
<mat-autocomplete #autoTaille="matAutocomplete" [displayWith]="displayTailleFn">
@for (option of filteredOptions.tailles | async; track option) {
<mat-option [value]="option" (onSelectionChange)="onSelTaille(option)">{{option.voile}} - {{option.taille}}</mat-option>
}
</mat-autocomplete>
<span matTextSuffix>ft<sup>2</sup></span>
@if (this.jumpForm.controls['taille'].status === 'INVALID') {
<mat-error>{{getErrorMessage('taille')}}</mat-error>
}
</mat-form-field>
</div>
<div class="col-lg-6 col-md-12">
<h4 class="border-bottom border-secondary mb-3 lh-lg">
Altitude
<mat-checkbox class="float-end" (change)="setLastAltitude($event.checked)" [checked]="useLast.altitude">
<mat-icon fontIcon="history" class="align-middle"></mat-icon>
</mat-checkbox>
</h4>
<mat-form-field appearance="fill" class="me-2" color="warn">
<mat-label>Hauteur de largage</mat-label>
<input matInput formControlName="hauteur" type="number" min="0" step="1">
@if (this.jumpForm.controls['hauteur'].status === 'INVALID') {
<mat-error>{{getErrorMessage('hauteur')}}</mat-error>
}
</mat-form-field>
<mat-form-field appearance="fill" class="me-2" color="warn">
<mat-label>Hauteur de déploiement</mat-label>
<input matInput formControlName="deploiement" type="number" min="0" step="1">
@if (this.jumpForm.controls['deploiement'].status === 'INVALID') {
<mat-error>{{getErrorMessage('deploiement')}}</mat-error>
}
</mat-form-field>
<h4 class="border-bottom border-secondary mb-3 lh-lg">
Discipline
<mat-checkbox class="float-end" (change)="setLastDiscipline($event.checked)" [checked]="useLast.discipline">
<mat-icon fontIcon="history" class="align-middle"></mat-icon>
</mat-checkbox>
</h4>
<mat-form-field appearance="fill" class="me-2" color="warn">
<mat-label>Categorie</mat-label>
<input matInput formControlName="categorie" [matAutocomplete]="autoCategorie">
<mat-autocomplete #autoCategorie="matAutocomplete" [displayWith]="displayCategorieFn">
@for (option of filteredOptions.categories | async; track option) {
<mat-option [value]="option" (onSelectionChange)="onSelCategorie(option)">{{option.categorie}}{{option.categorie && option.module ? ' - ' : ''}}{{option.module}}</mat-option>
}
</mat-autocomplete>
@if (this.jumpForm.controls['categorie'].status === 'INVALID') {
<mat-error>{{getErrorMessage('categorie')}}</mat-error>
}
</mat-form-field>
<mat-form-field appearance="fill" class="me-2" color="warn">
<mat-label>Module</mat-label>
<input matInput formControlName="module" [matAutocomplete]="autoModule">
<mat-autocomplete #autoModule="matAutocomplete" [displayWith]="displayModuleFn">
@for (option of filteredOptions.modules | async; track option) {
<mat-option [value]="option" (onSelectionChange)="onSelModule(option)">{{option.categorie}}{{option.categorie && option.module ? ' - ' : ''}}{{option.module}}</mat-option>
}
</mat-autocomplete>
@if (this.jumpForm.controls['module'].status === 'INVALID') {
<mat-error>{{getErrorMessage('module')}}</mat-error>
}
</mat-form-field>
<h4 class="border-bottom border-secondary mb-3 lh-lg">
Divers
<mat-checkbox class="float-end" (change)="setLastDivers($event.checked)" [checked]="useLast.divers">
<mat-icon fontIcon="history" class="align-middle"></mat-icon>
</mat-checkbox>
</h4>
<mat-form-field appearance="fill" class="me-2" color="warn">
<mat-label>Accessoires</mat-label>
<input matInput formControlName="accessoires">
@if (this.jumpForm.controls['accessoires'].status === 'INVALID') {
<mat-error>{{getErrorMessage('accessoires')}}</mat-error>
}
</mat-form-field>
<mat-form-field appearance="fill" class="me-2" color="warn">
<mat-label>Zone</mat-label>
<input matInput formControlName="zone">
@if (this.jumpForm.controls['zone'].status === 'INVALID') {
<mat-error>{{getErrorMessage('zone')}}</mat-error>
}
</mat-form-field>
</div>
<div class="col-lg-6 col-md-12">
<h4 class="border-bottom border-secondary mb-3 lh-lg">
Groupe
<mat-checkbox class="float-end" (change)="setLastGroupe($event.checked)" [checked]="useLast.groupe">
<mat-icon fontIcon="history" class="align-middle"></mat-icon>
</mat-checkbox>
</h4>
<mat-form-field appearance="fill" class="me-2" color="warn">
<mat-label>Sautants</mat-label>
<input matInput placeholder="Ajouter un sautant" [formControl]="tagField" (keyup.enter)="addSautant()">
</mat-form-field>
<mat-form-field appearance="fill" class="me-2" color="warn">
<mat-label>Nombre de participants</mat-label>
<input matInput formControlName="participants" type="number" min="0" step="1">
@if (this.jumpForm.controls['participants'].status === 'INVALID') {
<mat-error>{{getErrorMessage('participants')}}</mat-error>
}
</mat-form-field>
<div class="mb-2">
@for (sautant of jump.sautants; track sautant) {
<span class="badge rounded-pill bg-primary mb-2 me-1 pe-3 py-1 fw-normal text-dark">
<mat-icon class="align-middle pointer me-1" fontIcon="close" (click)="removeSautant(sautant)"></mat-icon>
{{ sautant }}
</span>
}
<span class="badge rounded-pill bg-accent mb-2 me-1 pe-3 py-1 fw-normal text-dark">
<mat-icon class="align-middle pointer me-1" fontIcon="person"></mat-icon>
{{ lastJump.author.username }}
</span>
</div>
</div>
<div class="col-lg-6 col-md-12">
<h4 class="border-bottom border-secondary mb-3 lh-lg">
Programme
<mat-checkbox class="float-end" (change)="setLastProgramme($event.checked)" [checked]="useLast.programme">
<mat-icon fontIcon="history" class="align-middle"></mat-icon>
</mat-checkbox>
</h4>
<mat-form-field appearance="fill" class="me-2 w-100" color="warn">
<mat-label>Programme</mat-label>
<textarea matInput formControlName="programme"></textarea>
@if (this.jumpForm.controls['programme'].status === 'INVALID') {
<mat-error>{{getErrorMessage('programme')}}</mat-error>
}
</mat-form-field>
<h4 class="border-bottom border-secondary mb-3 lh-lg">Vidéo</h4>
<mat-form-field appearance="fill" class="me-2" color="warn">
<mat-label>Dossier</mat-label>
<input matInput formControlName="dossier">
@if (this.jumpForm.controls['dossier'].status === 'INVALID') {
<mat-error>{{getErrorMessage('dossier')}}</mat-error>
}
</mat-form-field>
<mat-form-field appearance="fill" class="me-2" color="warn">
<mat-label>Video</mat-label>
<input matInput formControlName="video">
@if (this.jumpForm.controls['video'].status === 'INVALID') {
<mat-error>{{getErrorMessage('video')}}</mat-error>
}
</mat-form-field>
<span class="d-inline-block align-middle ms-1">{{ filepath }}{{ filename }}</span>
</div>
</form>
</div>
</mat-dialog-content>
<mat-dialog-actions>
<span class="flex-spacer"></span>
<button mat-button (click)="closeDialog()" class="me-1">Annuler</button>
<!--<button mat-button color="warn" [disabled]="!jumpForm.valid" [mat-dialog-close]="{jump: jump, params: params}" cdkFocusInitial>Enregistrer</button>-->
<button mat-button color="warn" [disabled]="!jumpForm.valid" (click)="submitForm()">Enregistrer</button>
</mat-dialog-actions>
@@ -0,0 +1,837 @@
import { Component, Inject, OnDestroy } from '@angular/core';
import { AsyncPipe, DatePipe } from '@angular/common';
import { FormBuilder, FormControl, FormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms';
import { MatAutocompleteModule } from '@angular/material/autocomplete';
import { MatButtonModule } from '@angular/material/button';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatDatepickerModule } from '@angular/material/datepicker';
import { MatDialogRef, MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatOptionModule } from '@angular/material/core';
import { MatSelectModule } from '@angular/material/select';
import { Observable, Subscription } from 'rxjs';
import { map, startWith } from 'rxjs/operators';
import { AeronefByImat, CanopyModelBySize, DropZoneByOaci, Jump, JumpAddParams, JumpByModule } from 'src/app/core/models';
import { AeronefsService, CanopiesService, DropZonesService, JumpsService } from 'src/app/core/services';
@Component({
selector: 'app-jump-add-dialog',
templateUrl: 'jump-add.dialog.html',
standalone: true,
imports: [
AsyncPipe,
DatePipe,
FormsModule,
ReactiveFormsModule,
MatAutocompleteModule,
MatButtonModule,
MatCheckboxModule,
MatDatepickerModule,
MatDialogModule,
MatFormFieldModule,
MatIconModule,
MatInputModule,
MatSelectModule,
MatOptionModule
]
})
export class JumpAddDialogComponent implements OnDestroy {
private _formChanges: Array<Subscription> = [];
private _aeronefByImat: Subscription = new Subscription();
private _canopyModelBySize: Subscription = new Subscription();
private _dropZoneByOaci: Subscription = new Subscription();
private _jumpByModule: Subscription = new Subscription();
public tagField = new FormControl<string>('', { nonNullable: true});
public jumpForm: FormGroup;
public jump: Jump;
public lastJump: Jump;
public nextJump: number;
public params: JumpAddParams;
public filename: string;
public filepath: string;
public useLast: {
all: boolean;
aeronef: boolean;
altitude: boolean;
discipline: boolean;
divers: boolean;
dropzone: boolean;
groupe: boolean;
programme: boolean;
voile: boolean;
} = {
all: false,
aeronef: false,
altitude: false,
discipline: false,
divers: false,
dropzone: false,
groupe: false,
programme: false,
voile: false
};
public inputOptions: {
aeronefs: Array<AeronefByImat>;
canopies: Array<CanopyModelBySize>;
dropzones: Array<DropZoneByOaci>;
categories: Array<JumpByModule>;
};
public filteredOptions: {
aeronefs: Observable<Array<AeronefByImat>>;
imats: Observable<Array<AeronefByImat>>;
canopies: Observable<Array<CanopyModelBySize>>;
tailles: Observable<Array<CanopyModelBySize>>;
dropzones: Observable<Array<DropZoneByOaci>>;
oaci: Observable<Array<DropZoneByOaci>>;
categories: Observable<Array<JumpByModule>>;
modules: Observable<Array<JumpByModule>>;
};
constructor(
public dialogRef: MatDialogRef<JumpAddDialogComponent>,
private fb: FormBuilder,
private _aeronefsService: AeronefsService,
private _canopiesService: CanopiesService,
private _dropZonesService: DropZonesService,
private _jumpsService: JumpsService,
@Inject(MAT_DIALOG_DATA) public data: {jump: Jump, lastJump: Jump}
) {
this.inputOptions = { aeronefs: [], canopies: [], dropzones: [], categories: [] };
//this.filteredOptions.aeronefs = this._aeronefsService.getAllByImat();
this.jump = data.jump;
this.lastJump = data.lastJump;
this.nextJump = (this.lastJump.numero + 1);
this.params = {
numeros: this.nextJump.toString(),
date: '',
video: true
};
this.jump.numero = this.nextJump;
const controlsConfig = {
date: ['', Validators.required],
numero: [this.nextJump, Validators.required],
lieu: ['', Validators.required],
oaci: '',
aeronef: ['', Validators.required],
imat: '',
hauteur: ['', Validators.required],
deploiement: '',
voile: '',
taille: ['', Validators.required],
categorie: '',
module: '',
participants: [1, Validators.required],
sautants: [[]],
programme: '',
accessoires: '',
zone: '',
dossier: '',
video: ''
};
this.jumpForm = this.fb.group(controlsConfig);
this.filename = this._computeFilename();
this.filepath = this._computeFilepath();
this._formChanges.push(
this.jumpForm.controls['module'].valueChanges.subscribe(() => {
this.filepath = this._computeFilepath();
}),
this.jumpForm.controls['numero'].valueChanges.subscribe(() => {
this.filename = this._computeFilename();
})
);
/*
this.filteredAeronefOptions = this.jumpForm.controls['aeronef'].valueChanges.pipe(
startWith(''),
map(value => {
const aeronef = typeof value === 'string' ? value : '';
return aeronef ? this._filterString(aeronef as string, this.aeronefOptions) : this.aeronefOptions.slice();
}),
);
this.filteredImatOptions = this.jumpForm.controls['imat'].valueChanges.pipe(
startWith(''),
map(value => {
const imat = typeof value === 'string' ? value : value?.imat;
return imat ? this._filterImat(imat as string) : this.imatOptions.slice();
}),
);
//this.filteredAeronefOptions = this._aeronefsService.getAllByImat();
*/
this.filteredOptions = {
aeronefs: this.jumpForm.controls['aeronef'].valueChanges.pipe(
startWith(''),
map(value => {
const aeronef = typeof value === 'string' ? value : value?.aeronef;
return aeronef ? this._filterImat(aeronef as string) : this.inputOptions.aeronefs.slice();
}),
),
imats: this.jumpForm.controls['imat'].valueChanges.pipe(
startWith(''),
map(value => {
const imat = typeof value === 'string' ? value : value?.imat;
return imat ? this._filterImat(imat as string) : this.inputOptions.aeronefs.slice();
}),
),
canopies: this.jumpForm.controls['voile'].valueChanges.pipe(
startWith(''),
map(value => {
const voile = typeof value === 'string' ? value : value?.voile;
return voile ? this._filterTaille(voile as string) : this.inputOptions.canopies.slice();
}),
),
tailles: this.jumpForm.controls['taille'].valueChanges.pipe(
startWith(''),
map(value => {
const taille = typeof value === 'string' ? value : value?.taille.toString();
return taille ? this._filterTaille(taille as string) : this.inputOptions.canopies.slice();
}),
),
dropzones: this.jumpForm.controls['lieu'].valueChanges.pipe(
startWith(''),
map(value => {
const lieu = typeof value === 'string' ? value : value?.lieu;
return lieu ? this._filterOaci(lieu as string) : this.inputOptions.dropzones.slice();
}),
),
oaci: this.jumpForm.controls['oaci'].valueChanges.pipe(
startWith(''),
map(value => {
const oaci = typeof value === 'string' ? value : value?.oaci;
return oaci ? this._filterOaci(oaci as string) : this.inputOptions.dropzones.slice();
}),
),
categories: this.jumpForm.controls['categorie'].valueChanges.pipe(
startWith(''),
map(value => {
const category = typeof value === 'string' ? value : value?.categorie;
return category ? this._filterModule(category as string) : this.inputOptions.categories.slice();
}),
),
modules: this.jumpForm.controls['module'].valueChanges.pipe(
startWith(''),
map(value => {
const module = typeof value === 'string' ? value : value?.module;
return module ? this._filterModule(module as string) : this.inputOptions.categories.slice();
}),
)
};
this._loadAeronefByImat();
this._loadCanopyModelBySize();
this._loadDropZoneByOaci();
this._loadJumpByModule();
}
ngOnDestroy() {
this._formChanges.forEach((sub: Subscription) => {
sub.unsubscribe();
});
this._aeronefByImat.unsubscribe();
this._canopyModelBySize.unsubscribe();
this._dropZoneByOaci.unsubscribe();
this._jumpByModule.unsubscribe();
}
public onDateChange(): void {
this.filename = this._computeFilename();
}
private _computeFilename(numero?: string): string {
let file: string = '';
const timestamp: number = Date.parse(this.jumpForm.controls['date'].value);
if (isNaN(timestamp) == false) {
const date: Date = new Date(timestamp);
file += `${(date.getFullYear() + '').padStart(2, '0')}-${((date.getMonth() + 1) + '').padStart(2, '0')}-${(date.getDate() + '').padStart(2, '0')}_`;
}
if (numero !== undefined) {
numero = (numero + '').padStart(5, '0');
} else {
numero = (this.jumpForm.controls['numero'].value + '').padStart(5, '0');
}
file += `S${numero}.mp4`;
this.jumpForm.controls['video'].setValue(file);
return file;
}
private _computeFilepath(): string {
let path: string = '/Volumes/Storage/Skydive/Videos/';
if (this.jumpForm.controls['module'].value != '') {
path += `${this.jumpForm.controls['module'].value}/`;
}
this.jumpForm.controls['dossier'].setValue(path);
return path;
}
private _isLastForAllSections() {
return (
this.useLast.aeronef
&& this.useLast.altitude
&& this.useLast.discipline
&& this.useLast.divers
&& this.useLast.dropzone
&& this.useLast.groupe
&& this.useLast.programme
&& this.useLast.voile
);
}
private _filterImat(imat: string): Array<AeronefByImat> {
const filterValue = imat.toLowerCase();
return this.inputOptions.aeronefs.filter(option => (option.aeronef.toLowerCase().includes(filterValue) || option.imat.toLowerCase().includes(filterValue)));
}
private _filterModule(module: string): Array<JumpByModule> {
const filterValue = module.toLowerCase();
return this.inputOptions.categories.filter(option => (option.categorie.toLowerCase().includes(filterValue) || option.module.toLowerCase().includes(filterValue)));
}
private _filterOaci(oaci: string): Array<DropZoneByOaci> {
const filterValue = oaci.toLowerCase();
return this.inputOptions.dropzones.filter(option => (option.lieu.toLowerCase().includes(filterValue) || option.oaci.toLowerCase().includes(filterValue)));
}
private _filterTaille(taille: string): Array<CanopyModelBySize> {
const filterValue = taille.toLowerCase();
return this.inputOptions.canopies.filter(option => (option.voile.toLowerCase().includes(filterValue) || option.taille.toString().includes(filterValue)));
}
private _loadAeronefByImat(): void {
const aeronefs$ = this._aeronefsService.getAllByImat();
this._aeronefByImat = aeronefs$.subscribe((aggregate: Array<AeronefByImat>) => {
/*
this.inputOptions.imats = aggregate.map((row: AeronefByImat) => {
if (this.inputOptions.aeronefs.indexOf(row.aeronef) === -1) {
this.inputOptions.aeronefs.push(row.aeronef);
}
return row;
});
*/
this.inputOptions.aeronefs = aggregate;
});
}
private _loadCanopyModelBySize(): void {
const canopies$ = this._canopiesService.getAllBySizeByModel();
this._canopyModelBySize = canopies$.subscribe((aggregate: Array<CanopyModelBySize>) => {
/*
this.inputOptions.tailles = aggregate.map((row: CanopyModelBySize) => {
if (this.inputOptions.canopies.indexOf(row.voile) === -1) {
this.inputOptions.canopies.push(row.voile);
}
return row;
});
*/
this.inputOptions.canopies = aggregate;
});
}
private _loadDropZoneByOaci(): void {
const dropzones$ = this._dropZonesService.getAllByOaci();
this._dropZoneByOaci = dropzones$.subscribe((aggregate: Array<DropZoneByOaci>) => {
/*
this.inputOptions.oaci = aggregate.map((row: DropZoneByOaci) => {
if (this.inputOptions.dropzones.indexOf(row.lieu) === -1) {
this.inputOptions.dropzones.push(row.lieu);
}
return row;
});
*/
this.inputOptions.dropzones = aggregate;
});
}
private _loadJumpByModule(): void {
const jumps$ = this._jumpsService.getAllByModule();
this._jumpByModule = jumps$.subscribe((aggregate: Array<JumpByModule>) => {
this.inputOptions.categories = aggregate;
});
}
private _setAllLast(checked: boolean) {
this.useLast.all = checked;
this.useLast.aeronef = checked;
this.useLast.altitude = checked;
this.useLast.discipline = checked;
this.useLast.divers = checked;
this.useLast.dropzone = checked;
this.useLast.groupe = checked;
this.useLast.programme = checked;
this.useLast.voile = checked;
}
addSautant(): void {
this.useLast.groupe = false;
this.useLast.all = this._isLastForAllSections();
const tag = this.tagField.value;
if (tag != null && tag.trim() !== '' && this.jump.sautants.indexOf(tag) < 0) {
this.jump.sautants.push(tag);
}
this.tagField.reset('');
const participants = (this.jump.sautants.length + 1);
this.jumpForm.controls['participants'].setValue(participants);
}
closeDialog(): void {
this._setAllLast(false);
//this.jump = <Jump>{};
//this.jump.numero = this.nextJump;
this.dialogRef.close();
}
displayAeronefFn(aeronef: AeronefByImat): string {
let res = '';
if (aeronef) {
if (typeof aeronef === 'string') {
res = aeronef;
} else {
res = aeronef.aeronef ? aeronef.aeronef : '';
}
}
return res;
}
displayImatFn(aeronef: AeronefByImat): string {
let res = '';
if (aeronef) {
if (typeof aeronef === 'string') {
res = aeronef;
} else {
res = aeronef.imat ? aeronef.imat : '';
}
}
return res;
}
displayLieuFn(dropzone: DropZoneByOaci): string {
//return dropzone && dropzone.oaci ? `${dropzone.lieu} - ${dropzone.oaci}` : '';
let res = '';
if (dropzone) {
if (typeof dropzone === 'string') {
res = dropzone;
} else {
res = dropzone.lieu ? dropzone.lieu : '';
}
}
//return dropzone && dropzone.lieu ? dropzone.lieu : '';
return res;
}
displayOaciFn(dropzone: DropZoneByOaci): string {
//return dropzone && dropzone.oaci ? `${dropzone.lieu} - ${dropzone.oaci}` : '';
let res = '';
if (dropzone) {
if (typeof dropzone === 'string') {
res = dropzone;
} else {
res = dropzone.oaci ? dropzone.oaci : '';
}
}
//return dropzone && dropzone.oaci ? dropzone.oaci : '';
return res;
}
displayTailleFn(canopy: CanopyModelBySize): string {
let res = '';
if (canopy) {
if (typeof canopy === 'string') {
res = canopy;
} else {
res = canopy.taille ? canopy.taille.toString() : '';
}
}
return res;
}
displayVoileFn(canopy: CanopyModelBySize): string {
let res = '';
if (canopy) {
if (typeof canopy === 'string') {
res = canopy;
} else {
res = canopy.voile ? canopy.voile : '';
}
}
return res;
}
displayCategorieFn(jump: JumpByModule): string {
let res = '';
if (jump) {
if (typeof jump === 'string') {
res = jump;
} else {
res = jump.categorie ? jump.categorie : '';
}
}
return res;
}
displayModuleFn(jump: JumpByModule): string {
let res = '';
if (jump) {
if (typeof jump === 'string') {
res = jump;
} else {
res = jump.module ? jump.module : '';
}
}
return res;
}
getErrorMessage(name: string): string {
if (this.jumpForm.controls[name].errors !== null) {
return 'Ce champ est requis.';
}
return '';
}
onSelAeronef(aeronef: AeronefByImat) {
if (aeronef && aeronef.aeronef) {
//this.jumpForm.controls['imat'].setValue(aeronef.imat);
this.jumpForm.controls['imat'].setValue(aeronef);
}
}
onSelImat(aeronef: AeronefByImat) {
if (aeronef && aeronef.imat) {
//this.jumpForm.controls['aeronef'].setValue(aeronef.aeronef);
this.jumpForm.controls['aeronef'].setValue(aeronef);
}
}
onSelLieu(dropzone: DropZoneByOaci) {
if (dropzone && dropzone.oaci) {
//this.jumpForm.controls['oaci'].setValue(dropzone.oaci);
this.jumpForm.controls['oaci'].setValue(dropzone);
}
}
onSelOaci(dropzone: DropZoneByOaci) {
if (dropzone && dropzone.lieu) {
//this.jumpForm.controls['lieu'].setValue(dropzone.lieu);
this.jumpForm.controls['lieu'].setValue(dropzone);
}
}
onSelTaille(canopy: CanopyModelBySize) {
if (canopy && canopy.voile) {
//this.jumpForm.controls['voile'].setValue(canopy.voile);
this.jumpForm.controls['voile'].setValue(canopy);
}
}
onSelVoile(canopy: CanopyModelBySize) {
if (canopy && canopy.taille) {
//this.jumpForm.controls['taille'].setValue(canopy.taille.toString());
this.jumpForm.controls['taille'].setValue(canopy);
}
}
onSelCategorie(jump: JumpByModule) {
if (jump && jump.categorie) {
this.jumpForm.controls['module'].setValue(jump);
}
}
onSelModule(jump: JumpByModule) {
if (jump && jump.module) {
this.jumpForm.controls['categorie'].setValue(jump);
}
}
removeSautant(index: string): void {
this.useLast.groupe = false;
this.useLast.all = this._isLastForAllSections();
this.jump.sautants = this.jump.sautants.filter((sautant) => sautant !== index);
const participants = (this.jump.sautants.length + 1);
this.jumpForm.controls['participants'].setValue(participants);
}
setLastJump(checked: boolean) {
if (checked) {
this._setAllLast(true);
this.jumpForm.controls['lieu'].setValue(this.lastJump.lieu);
this.jumpForm.controls['oaci'].setValue(this.lastJump.oaci);
this.jumpForm.controls['aeronef'].setValue(this.lastJump.aeronef);
this.jumpForm.controls['imat'].setValue(this.lastJump.imat);
this.jumpForm.controls['hauteur'].setValue(this.lastJump.hauteur);
this.jumpForm.controls['deploiement'].setValue(this.lastJump.deploiement);
this.jumpForm.controls['voile'].setValue(this.lastJump.voile);
this.jumpForm.controls['taille'].setValue(this.lastJump.taille?.toString());
this.jumpForm.controls['categorie'].setValue(this.lastJump.categorie);
this.jumpForm.controls['module'].setValue(this.lastJump.module);
this.jumpForm.controls['participants'].setValue(this.lastJump.participants);
//this.jumpForm.controls['sautants'].setValue(this.lastJump.sautants);
this.jumpForm.controls['programme'].setValue(this.lastJump.programme);
this.jumpForm.controls['accessoires'].setValue(this.lastJump.accessoires);
this.jumpForm.controls['zone'].setValue(this.lastJump.zone);
this.jump.sautants = this.lastJump.sautants;
/*
Object.assign(
this.jump,
{
date: this.lastJump.date,
numero: this.lastJump.numero,
lieu: this.lastJump.lieu,
oaci: this.lastJump.oaci,
aeronef: this.lastJump.aeronef,
imat: this.lastJump.imat,
hauteur: this.lastJump.hauteur,
deploiement: this.lastJump.deploiement,
voile: this.lastJump.voile,
taille: this.lastJump.taille,
categorie: this.lastJump.categorie,
module: this.lastJump.module,
participants: this.lastJump.participants,
sautants: this.lastJump.sautants,
programme: this.lastJump.programme,
accessoires: this.lastJump.accessoires,
zone: this.lastJump.zone,
}
);
*/
this.jump.date = '';
} else {
this._setAllLast(false);
this.jumpForm.controls['lieu'].setValue('');
this.jumpForm.controls['oaci'].setValue('');
this.jumpForm.controls['aeronef'].setValue('');
this.jumpForm.controls['imat'].setValue('');
this.jumpForm.controls['hauteur'].setValue('');
this.jumpForm.controls['deploiement'].setValue('');
this.jumpForm.controls['voile'].setValue('');
this.jumpForm.controls['taille'].setValue('');
this.jumpForm.controls['categorie'].setValue('');
this.jumpForm.controls['module'].setValue('');
this.jumpForm.controls['participants'].setValue(1);
//this.jumpForm.controls['sautants'].setValue('');
this.jumpForm.controls['programme'].setValue('');
this.jumpForm.controls['accessoires'].setValue('');
this.jumpForm.controls['zone'].setValue('');
this.jump.sautants = [];
}
this.jump.numero = this.nextJump;
}
setLastAeronef(checked: boolean = false) {
if (checked) {
this.useLast.aeronef = true;
this.jumpForm.controls['aeronef'].setValue(this.lastJump.aeronef);
this.jumpForm.controls['imat'].setValue(this.lastJump.imat);
} else {
this.useLast.aeronef = false;
this.jumpForm.controls['aeronef'].setValue('');
this.jumpForm.controls['imat'].setValue('');
}
this.useLast.all = this._isLastForAllSections();
}
setLastAltitude(checked: boolean = false) {
if (checked) {
this.useLast.altitude = true;
this.jumpForm.controls['hauteur'].setValue(this.lastJump.hauteur);
this.jumpForm.controls['deploiement'].setValue(this.lastJump.deploiement);
} else {
this.useLast.altitude = false;
this.jumpForm.controls['hauteur'].setValue('');
this.jumpForm.controls['deploiement'].setValue('');
}
this.useLast.all = this._isLastForAllSections();
}
setLastDiscipline(checked: boolean = false) {
if (checked) {
this.useLast.discipline = true;
this.jumpForm.controls['categorie'].setValue(this.lastJump.categorie);
this.jumpForm.controls['module'].setValue(this.lastJump.module);
} else {
this.useLast.discipline = false;
this.jumpForm.controls['categorie'].setValue('');
this.jumpForm.controls['module'].setValue('');
}
this.useLast.all = this._isLastForAllSections();
}
setLastDivers(checked: boolean = false) {
if (checked) {
this.useLast.divers = true;
this.jumpForm.controls['accessoires'].setValue(this.lastJump.accessoires);
this.jumpForm.controls['zone'].setValue(this.lastJump.zone);
} else {
this.useLast.divers = false;
this.jumpForm.controls['accessoires'].setValue('');
this.jumpForm.controls['zone'].setValue('');
}
this.useLast.all = this._isLastForAllSections();
}
setLastDropzone(checked: boolean = false) {
if (checked) {
this.useLast.dropzone = true;
this.jumpForm.controls['lieu'].setValue(this.lastJump.lieu);
this.jumpForm.controls['oaci'].setValue(this.lastJump.oaci);
} else {
this.useLast.dropzone = false;
this.jumpForm.controls['lieu'].setValue('');
this.jumpForm.controls['oaci'].setValue('');
}
this.useLast.all = this._isLastForAllSections();
}
setLastGroupe(checked: boolean = false) {
if (checked) {
this.useLast.groupe = true;
this.jumpForm.controls['participants'].setValue(this.lastJump.participants);
//this.jumpForm.controls['sautants'].setValue(this.lastJump.sautants);
this.jump.sautants = this.lastJump.sautants;
} else {
this.useLast.groupe = false;
this.jumpForm.controls['participants'].setValue(1);
//this.jumpForm.controls['sautants'].setValue('');
this.jump.sautants = [];
}
this.useLast.all = this._isLastForAllSections();
}
setLastProgramme(checked: boolean = false) {
if (checked) {
this.useLast.programme = true;
this.jumpForm.controls['programme'].setValue(this.lastJump.participants);
} else {
this.useLast.programme = false;
this.jumpForm.controls['programme'].setValue('');
}
this.useLast.all = this._isLastForAllSections();
}
setLastVoile(checked: boolean = false) {
if (checked) {
this.useLast.voile = true;
this.jumpForm.controls['voile'].setValue(this.lastJump.voile);
this.jumpForm.controls['taille'].setValue(this.lastJump.taille?.toString());
} else {
this.useLast.voile = false;
this.jumpForm.controls['voile'].setValue('');
this.jumpForm.controls['taille'].setValue('');
}
this.useLast.all = this._isLastForAllSections();
}
submitForm(): void {
/*
* Création d'un tableau de saut contenant de 1 à n saut en fonction
* de la valeur du champ numero du formulaire jumpForm
*/
const jumps: Array<Jump> = [];
const jump: Jump = {} as Jump;
const numeros: Array<number> = [];
let date: Date;
/* Controle de la validité du champ date et conversion au format ISO */
const timestamp: number = Date.parse(this.jumpForm.controls['date'].value);
if (isNaN(timestamp) == false) {
date = new Date(timestamp);
} else {
date = new Date();
}
let value = this.jumpForm.controls['numero'].value;
if (typeof value !== 'string') {
value = value.toString();
}
if (isNaN(+value) || value.split('+').length == 2) {
/*
* 'numero' n'est pas un entier , il s'agit donc d'une plage, d'une quantité ou de plusieurs valeur
* decomposer la valeur 'numero' et ajouter chaque sauts au tableau retourné par la fenêtre dialog
*/
let jumpNum: number = this.lastJump.numero;
const elements: string[] = this.jumpForm.controls['numero'].value.split(';');
elements.forEach((element: string) => {
if (element == '') {
jumpNum ++;
element = jumpNum.toString();
}
const values: string[] = element.split('-');
if (values.length == 2) {
const start: number = parseInt(values[0]);
const stop: number = parseInt(values[1]);
const diff: number = (stop - start);
/* Ajout de saut par plage */
for (let index = 0; index <= diff; index++) {
jumpNum = (start + index);
numeros.push(jumpNum);
}
} else {
const quantity: string[] = values[0].split('+');
if (quantity.length == 2) {
/* Ajout de sauts par quantité */
//const start: number = jumpNum;
for (let index = 1; index <= parseInt(quantity[1]); index++) {
jumpNum ++;
numeros.push(jumpNum);
}
} else {
/* Ajout de sauts par numero */
jumpNum = parseInt(quantity[0]);
numeros.push(jumpNum);
}
}
});
} else {
/* 'numero' est un entier */
numeros.push(this.jumpForm.controls['numero'].value);
}
/* Valeurs communes aux 1 à n saut(s) à ajouter */
jump.date = date.toISOString();
jump.lieu = typeof this.jumpForm.controls['lieu'].value === 'string' ? this.jumpForm.controls['lieu'].value : this.jumpForm.controls['lieu'].value.lieu;
jump.oaci = typeof this.jumpForm.controls['oaci'].value === 'string' ? this.jumpForm.controls['oaci'].value : this.jumpForm.controls['oaci'].value.oaci;
jump.aeronef = typeof this.jumpForm.controls['aeronef'].value === 'string' ? this.jumpForm.controls['aeronef'].value : this.jumpForm.controls['aeronef'].value.aeronef;
jump.imat = typeof this.jumpForm.controls['imat'].value === 'string' ? this.jumpForm.controls['imat'].value : this.jumpForm.controls['imat'].value.imat;
jump.voile = typeof this.jumpForm.controls['voile'].value === 'string' ? this.jumpForm.controls['voile'].value : this.jumpForm.controls['voile'].value.voile;
jump.taille = typeof this.jumpForm.controls['taille'].value === 'string' ? this.jumpForm.controls['taille'].value : this.jumpForm.controls['taille'].value.taille;
jump.categorie = typeof this.jumpForm.controls['categorie'].value === 'string' ? this.jumpForm.controls['categorie'].value : this.jumpForm.controls['categorie'].value.categorie;
jump.module = typeof this.jumpForm.controls['module'].value === 'string' ? this.jumpForm.controls['module'].value : this.jumpForm.controls['module'].value.module;
jump.hauteur = this.jumpForm.controls['hauteur'].value;
jump.deploiement = this.jumpForm.controls['deploiement'].value;
jump.participants = this.jumpForm.controls['participants'].value;
jump.sautants = this.jump.sautants;
//jump.sautants = this.jumpForm.controls['sautants'].value;
jump.programme = this.jumpForm.controls['programme'].value;
jump.accessoires = this.jumpForm.controls['accessoires'].value;
jump.zone = this.jumpForm.controls['zone'].value;
//jump.dossier = this.jumpForm.controls['dossier'].value;
//jump.video = this.jumpForm.controls['video'].value;
/* Ajout du saut au tableau à retourner */
numeros.forEach((numero: number) => {
const item: Jump = {} as Jump;
Object.assign(item, jump);
item.numero = numero;
item.dossier = this._computeFilepath();
item.video = this._computeFilename(numero.toString());
jumps.push(item);
//console.log(item);
});
//console.log(jumps);
/* Reset all 'last jump values' booleans */
this._setAllLast(false);
/* close the dialog with result */
this.dialogRef.close(jumps);
/*
// Reset all 'last jump values' booleans
this._setAllLast(false);
// update the model
this.updateJump(this.jumpForm.value);
// close the dialog with result
this.dialogRef.close(this.jump);
*/
}
updateJump(values: NonNullable<unknown>): void {
Object.assign(this.jump, values);
}
}
@@ -0,0 +1,64 @@
<h2 mat-dialog-title class="bg-danger border-bottom border-light-subtle d-flex mb-3 pt-3 pb-2">
Supprimer le saut n°{{jump.numero}}<span class="flex-spacer"></span>
<mat-icon fontIcon="close" class="pointer" (click)="closeDialog()"></mat-icon>
</h2>
<mat-dialog-content>
<div class="container">
<div class="mt-2 mb-4 text-center text-danger border-danger alert alert-danger">
<p class="fs-6 m-0">
Veuillez confirmer la suppression de ce saut.<br />
<span class="fw-semibold">Attention</span>, cette <span class="fw-semibold">action</span> est <span class="fw-semibold">irréversible</span>.
</p>
</div>
<h3 class="border-bottom border-light-subtle pb-2 mb-3">Données du saut :</h3>
<div class="row mb-3">
<div class="col-lg-6 col-md-12">
<dl class="dl-horizontal">
<dt class="text-accent fw-normal">numero:</dt>
<dd>{{jump.numero}}</dd>
<dt class="text-accent fw-normal">lieu:</dt>
<dd>{{jump.lieu}}</dd>
<dt class="text-accent fw-normal">aeronef:</dt>
<dd>{{jump.aeronef}}</dd>
<dt class="text-accent fw-normal">voile:</dt>
<dd>{{jump.voile}}</dd>
<dt class="text-accent fw-normal">hauteur:</dt>
<dd>{{jump.hauteur}} m</dd>
<dt class="text-accent fw-normal">categorie:</dt>
<dd>{{jump.categorie}}</dd>
<dt class="text-accent fw-normal">accessoires:</dt>
<dd>{{jump.accessoires}}</dd>
<dt class="text-accent fw-normal">dossier:</dt>
<dd>{{jump.dossier}}</dd>
<dt class="text-accent fw-normal">programme:</dt>
<dd>{{jump.programme}}</dd>
</dl>
</div>
<div class="col-lg-6 col-md-12">
<dl class="dl-horizontal">
<dt class="text-accent fw-normal">date:</dt>
<dd>{{jump.date | date: 'mediumDate'}}</dd>
<dt class="text-accent fw-normal">oaci:</dt>
<dd>{{jump.oaci}}</dd>
<dt class="text-accent fw-normal">imat:</dt>
<dd>{{jump.imat}}</dd>
<dt class="text-accent fw-normal">taille:</dt>
<dd>{{jump.taille}} ft<sup>2</sup></dd>
<dt class="text-accent fw-normal">participants:</dt>
<dd>{{jump.participants}}</dd>
<dt class="text-accent fw-normal">module:</dt>
<dd>{{jump.module}}</dd>
<dt class="text-accent fw-normal">zone:</dt>
<dd>{{jump.zone}}</dd>
<dt class="text-accent fw-normal">video:</dt>
<dd>{{jump.video}}</dd>
</dl>
</div>
</div>
</div>
</mat-dialog-content>
<mat-dialog-actions>
<span class="flex-spacer"></span>
<button mat-button (click)="closeDialog()" class="me-1">Annuler</button>
<button mat-button color="accent" [mat-dialog-close]="jump" cdkFocusInitial>Supprimer</button>
</mat-dialog-actions>
@@ -1,15 +1,19 @@
import { Component, Inject } from '@angular/core'; import { Component, Inject } from '@angular/core';
import { DatePipe } from '@angular/common';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatDialogRef, MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog'; import { MatDialogRef, MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';
import { MatDividerModule } from '@angular/material/divider'; import { MatIconModule } from '@angular/material/icon';
import { Jump } from 'src/app/core/models'; import { Jump } from 'src/app/core/models';
@Component({ @Component({
selector: 'huapp-jump-delete-dialog', selector: 'app-jump-delete-dialog',
templateUrl: 'jump-delete.dialog.html', templateUrl: 'jump-delete.dialog.html',
standalone: true, standalone: true,
imports: [MatDialogModule, MatButtonModule, MatDividerModule] imports: [
DatePipe,
MatDialogModule, MatButtonModule, MatIconModule
]
}) })
export class JumpDeleteDialogComponent { export class JumpDeleteDialogComponent {
constructor( constructor(
@@ -17,7 +21,7 @@ export class JumpDeleteDialogComponent {
@Inject(MAT_DIALOG_DATA) public jump: Jump @Inject(MAT_DIALOG_DATA) public jump: Jump
) { } ) { }
onCloseDialog(): void { closeDialog(): void {
this.dialogRef.close(); this.dialogRef.close();
} }
} }
@@ -0,0 +1,219 @@
<h2 mat-dialog-title class="bg-primary border-bottom border-light-subtle d-flex mb-3 pt-3 pb-2">
Modifier un saut<span class="flex-spacer"></span>
<mat-icon fontIcon="close" class="pointer" (click)="closeDialog()"></mat-icon>
</h2>
<mat-dialog-content>
<div class="container">
<form class="row" [formGroup]="jumpForm" (ngSubmit)="submitForm()">
<div class="col-xs-12">
<mat-form-field appearance="outline" class="mt-2 me-2" color="primary">
<mat-label>Numéro du saut</mat-label>
<input matInput formControlName="numero">
</mat-form-field>
<mat-form-field appearance="outline" class="mt-2 me-3" color="primary">
<mat-label>Choisissez une date</mat-label>
<input matInput [matDatepicker]="datepicker" formControlName="date" />
<mat-hint>MM/DD/YYYY</mat-hint>
<mat-datepicker-toggle matIconSuffix [for]="datepicker"></mat-datepicker-toggle>
<mat-datepicker #datepicker>
<mat-datepicker-actions>
<button mat-button matDatepickerCancel>Annuler</button>
<button mat-raised-button color="primary" matDatepickerApply>Appliquer</button>
</mat-datepicker-actions>
</mat-datepicker>
</mat-form-field>
</div>
<div class="col-lg-6 col-md-12">
<h4 class="border-bottom border-primary pb-1 mb-3">Dropzone</h4>
<mat-form-field appearance="fill" class="me-2">
<mat-label>Lieu</mat-label>
<input matInput formControlName="lieu" [matAutocomplete]="autoDropzone">
<mat-autocomplete #autoDropzone="matAutocomplete" [displayWith]="displayLieuFn">
@for (option of filteredOptions.dropzones | async; track option) {
<mat-option [value]="option" (onSelectionChange)="onSelLieu(option)">{{option.lieu}} - {{option.oaci}}</mat-option>
}
</mat-autocomplete>
@if (this.jumpForm.controls['lieu'].status === 'INVALID') {
<mat-error>{{getErrorMessage('lieu')}}</mat-error>
}
</mat-form-field>
<mat-form-field appearance="fill" class="me-2">
<mat-label>Oaci</mat-label>
<input matInput formControlName="oaci" [matAutocomplete]="autoOaci">
<mat-autocomplete #autoOaci="matAutocomplete" [displayWith]="displayOaciFn">
@for (option of filteredOptions.oaci | async; track option) {
<mat-option [value]="option" (onSelectionChange)="onSelOaci(option)">{{option.lieu}} - {{option.oaci}}</mat-option>
}
</mat-autocomplete>
@if (this.jumpForm.controls['oaci'].status === 'INVALID') {
<mat-error>{{getErrorMessage('oaci')}}</mat-error>
}
</mat-form-field>
<h4 class="border-bottom border-primary pb-1 mb-3">Aéronef</h4>
<mat-form-field appearance="fill" class="me-2">
<mat-label>Aéronef</mat-label>
<input matInput formControlName="aeronef" [matAutocomplete]="autoAeronef">
<mat-autocomplete #autoAeronef="matAutocomplete" [displayWith]="displayAeronefFn">
@for (option of filteredOptions.aeronefs | async; track option) {
<mat-option [value]="option" (onSelectionChange)="onSelAeronef(option)">{{option.aeronef}} - {{option.imat}}</mat-option>
}
</mat-autocomplete>
@if (this.jumpForm.controls['aeronef'].status === 'INVALID') {
<mat-error>{{getErrorMessage('aeronef')}}</mat-error>
}
</mat-form-field>
<mat-form-field appearance="fill" class="me-2">
<mat-label>Imatriculation</mat-label>
<input matInput formControlName="imat" [matAutocomplete]="autoImat">
<mat-autocomplete #autoImat="matAutocomplete" [displayWith]="displayImatFn">
@for (option of filteredOptions.imats | async; track option) {
<mat-option [value]="option" (onSelectionChange)="onSelImat(option)">{{option.aeronef}} - {{option.imat}}</mat-option>
}
</mat-autocomplete>
@if (this.jumpForm.controls['imat'].status === 'INVALID') {
<mat-error>{{getErrorMessage('imat')}}</mat-error>
}
</mat-form-field>
<h4 class="border-bottom border-primary pb-1 mb-3">Voile</h4>
<mat-form-field appearance="fill" class="me-2">
<mat-label>Modèle</mat-label>
<input matInput formControlName="voile" [matAutocomplete]="autoCanopy">
<mat-autocomplete #autoCanopy="matAutocomplete" [displayWith]="displayVoileFn">
@for (option of filteredOptions.canopies | async; track option) {
<mat-option [value]="option" (onSelectionChange)="onSelVoile(option)">{{option.voile}} - {{option.taille}}</mat-option>
}
</mat-autocomplete>
@if (this.jumpForm.controls['voile'].status === 'INVALID') {
<mat-error>{{getErrorMessage('voile')}}</mat-error>
}
</mat-form-field>
<mat-form-field appearance="fill" class="me-2">
<mat-label>Taille en ft<sup>2</sup></mat-label>
<input matInput formControlName="taille" [matAutocomplete]="autoTaille">
<mat-autocomplete #autoTaille="matAutocomplete" [displayWith]="displayTailleFn">
@for (option of filteredOptions.tailles | async; track option) {
<mat-option [value]="option" (onSelectionChange)="onSelTaille(option)">{{option.voile}} - {{option.taille}}</mat-option>
}
</mat-autocomplete>
<span matTextSuffix>ft<sup>2</sup></span>
@if (this.jumpForm.controls['taille'].status === 'INVALID') {
<mat-error>{{getErrorMessage('taille')}}</mat-error>
}
</mat-form-field>
</div>
<div class="col-lg-6 col-md-12">
<h4 class="border-bottom border-primary pb-1 mb-3">Altitude</h4>
<mat-form-field appearance="fill" class="me-2">
<mat-label>Hauteur de largage</mat-label>
<input matInput formControlName="hauteur" type="number" min="0" step="1">
@if (this.jumpForm.controls['hauteur'].status === 'INVALID') {
<mat-error>{{getErrorMessage('hauteur')}}</mat-error>
}
</mat-form-field>
<mat-form-field appearance="fill" class="me-2">
<mat-label>Hauteur de déploiement</mat-label>
<input matInput formControlName="deploiement" type="number" min="0" step="1">
@if (this.jumpForm.controls['deploiement'].status === 'INVALID') {
<mat-error>{{getErrorMessage('deploiement')}}</mat-error>
}
</mat-form-field>
<h4 class="border-bottom border-primary pb-1 mb-3">Discipline</h4>
<mat-form-field appearance="fill" class="me-2">
<mat-label>Categorie</mat-label>
<input matInput formControlName="categorie" [matAutocomplete]="autoCategorie">
<mat-autocomplete #autoCategorie="matAutocomplete" [displayWith]="displayCategorieFn">
@for (option of filteredOptions.categories | async; track option) {
<mat-option [value]="option" (onSelectionChange)="onSelCategorie(option)">{{option.categorie}}{{option.categorie && option.module ? ' - ' : ''}}{{option.module}}</mat-option>
}
</mat-autocomplete>
@if (this.jumpForm.controls['categorie'].status === 'INVALID') {
<mat-error>{{getErrorMessage('categorie')}}</mat-error>
}
</mat-form-field>
<mat-form-field appearance="fill" class="me-2">
<mat-label>Module</mat-label>
<input matInput formControlName="module" [matAutocomplete]="autoModule">
<mat-autocomplete #autoModule="matAutocomplete" [displayWith]="displayModuleFn">
@for (option of filteredOptions.modules | async; track option) {
<mat-option [value]="option" (onSelectionChange)="onSelModule(option)">{{option.categorie}}{{option.categorie && option.module ? ' - ' : ''}}{{option.module}}</mat-option>
}
</mat-autocomplete>
@if (this.jumpForm.controls['module'].status === 'INVALID') {
<mat-error>{{getErrorMessage('module')}}</mat-error>
}
</mat-form-field>
<h4 class="border-bottom border-primary pb-1 mb-3">Divers</h4>
<mat-form-field appearance="fill" class="me-2">
<mat-label>Accessoires</mat-label>
<input matInput formControlName="accessoires">
@if (this.jumpForm.controls['accessoires'].status === 'INVALID') {
<mat-error>{{getErrorMessage('accessoires')}}</mat-error>
}
</mat-form-field>
<mat-form-field appearance="fill" class="me-2">
<mat-label>Zone</mat-label>
<input matInput formControlName="zone">
@if (this.jumpForm.controls['zone'].status === 'INVALID') {
<mat-error>{{getErrorMessage('zone')}}</mat-error>
}
</mat-form-field>
</div>
<div class="col-lg-6 col-md-12">
<h4 class="border-bottom border-primary pb-1 mb-3">Groupe</h4>
<mat-form-field appearance="fill" class="me-2">
<mat-label>Sautants</mat-label>
<input matInput placeholder="Ajouter un sautant" [formControl]="tagField" (keyup.enter)="addSautant()">
</mat-form-field>
<mat-form-field appearance="fill" class="me-2">
<mat-label>Nombre de participants</mat-label>
<input matInput formControlName="participants" type="number" min="0" step="1">
@if (this.jumpForm.controls['participants'].status === 'INVALID') {
<mat-error>{{getErrorMessage('participants')}}</mat-error>
}
</mat-form-field>
<div class="mb-2">
@for (sautant of jump.sautants; track sautant) {
<span class="badge rounded-pill bg-primary mb-2 me-1 pe-3 py-1 fw-normal text-dark">
<mat-icon class="align-middle pointer me-1" fontIcon="close" (click)="removeSautant(sautant)"></mat-icon>
{{ sautant }}
</span>
}
<span class="badge rounded-pill bg-accent mb-2 me-1 pe-3 py-1 fw-normal text-dark">
<mat-icon class="align-middle pointer me-1" fontIcon="person"></mat-icon>
{{ jump.author.username }}
</span>
</div>
</div>
<div class="col-lg-6 col-md-12">
<h4 class="border-bottom border-primary pb-1 mb-3">Programme</h4>
<mat-form-field appearance="fill" class="me-2 w-100">
<mat-label>Programme</mat-label>
<textarea matInput formControlName="programme"></textarea>
@if (this.jumpForm.controls['programme'].status === 'INVALID') {
<mat-error>{{getErrorMessage('programme')}}</mat-error>
}
</mat-form-field>
<h4 class="border-bottom border-primary pb-1 mb-3">Vidéo</h4>
<mat-form-field appearance="fill" class="me-2">
<mat-label>Dossier</mat-label>
<input matInput formControlName="dossier">
@if (this.jumpForm.controls['dossier'].status === 'INVALID') {
<mat-error>{{getErrorMessage('dossier')}}</mat-error>
}
</mat-form-field>
<mat-form-field appearance="fill" class="me-2">
<mat-label>Video</mat-label>
<input matInput formControlName="video">
@if (this.jumpForm.controls['video'].status === 'INVALID') {
<mat-error>{{getErrorMessage('video')}}</mat-error>
}
</mat-form-field>
</div>
</form>
</div>
</mat-dialog-content>
<mat-dialog-actions>
<span class="flex-spacer"></span>
<button mat-button (click)="closeDialog()" class="me-1">Annuler</button>
<button mat-button color="primary" [disabled]="!jumpForm.valid" (click)="submitForm()" cdkFocusInitial>Enregistrer</button>
</mat-dialog-actions>
@@ -0,0 +1,407 @@
import { Component, Inject, OnDestroy } from '@angular/core';
import { AsyncPipe, DatePipe } from '@angular/common';
import { FormBuilder, FormControl, FormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms';
import { MatAutocompleteModule } from '@angular/material/autocomplete';
import { MatButtonModule } from '@angular/material/button';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatDatepickerModule } from '@angular/material/datepicker';
import { MatDialogRef, MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatOptionModule } from '@angular/material/core';
import { MatSelectModule } from '@angular/material/select';
import { Observable, Subscription } from 'rxjs';
import { map, startWith } from 'rxjs/operators';
import { AeronefByImat, CanopyModelBySize, DropZoneByOaci, Jump, JumpByModule } from 'src/app/core/models';
import { AeronefsService, CanopiesService, DropZonesService, JumpsService } from 'src/app/core/services';
@Component({
selector: 'app-jump-edit-dialog',
templateUrl: 'jump-edit.dialog.html',
standalone: true,
imports: [
AsyncPipe,
DatePipe,
FormsModule,
ReactiveFormsModule,
MatAutocompleteModule,
MatButtonModule,
MatCheckboxModule,
MatDatepickerModule,
MatDialogModule,
MatFormFieldModule,
MatIconModule,
MatInputModule,
MatSelectModule,
MatOptionModule
]
})
export class JumpEditDialogComponent implements OnDestroy {
private _aeronefByImat: Subscription = new Subscription();
private _canopyModelBySize: Subscription = new Subscription();
private _dropZoneByOaci: Subscription = new Subscription();
private _jumpByModule: Subscription = new Subscription();
public tagField = new FormControl<string>('', { nonNullable: true});
public jumpForm: FormGroup;
public inputOptions: {
aeronefs: Array<AeronefByImat>;
canopies: Array<CanopyModelBySize>;
dropzones: Array<DropZoneByOaci>;
categories: Array<JumpByModule>;
};
public filteredOptions: {
aeronefs: Observable<Array<AeronefByImat>>;
imats: Observable<Array<AeronefByImat>>;
canopies: Observable<Array<CanopyModelBySize>>;
tailles: Observable<Array<CanopyModelBySize>>;
dropzones: Observable<Array<DropZoneByOaci>>;
oaci: Observable<Array<DropZoneByOaci>>;
categories: Observable<Array<JumpByModule>>;
modules: Observable<Array<JumpByModule>>;
};
constructor(
public dialogRef: MatDialogRef<JumpEditDialogComponent>,
private fb: FormBuilder,
private _aeronefsService: AeronefsService,
private _canopiesService: CanopiesService,
private _dropZonesService: DropZonesService,
private _jumpsService: JumpsService,
@Inject(MAT_DIALOG_DATA) public jump: Jump
) {
this.inputOptions = { aeronefs: [], canopies: [], dropzones: [], categories: [] };
const controlsConfig = {
slug: [this.jump.slug, Validators.required],
date: [this.jump.date, Validators.required],
numero: [this.jump.numero, Validators.required],
lieu: [this.jump.lieu, Validators.required],
oaci: this.jump.oaci,
aeronef: [this.jump.aeronef, Validators.required],
imat: this.jump.imat,
hauteur: [this.jump.hauteur, Validators.required],
deploiement: this.jump.deploiement,
voile: this.jump.voile,
taille: [this.jump.taille?.toString(), Validators.required],
categorie: this.jump.categorie,
module: this.jump.module,
participants: [this.jump.participants, Validators.required],
programme: this.jump.programme,
accessoires: this.jump.accessoires,
zone: this.jump.zone,
dossier: this.jump.dossier,
video: this.jump.video
};
this.jumpForm = this.fb.group(controlsConfig);
this.filteredOptions = {
aeronefs: this.jumpForm.controls['aeronef'].valueChanges.pipe(
startWith(''),
map(value => {
const aeronef = typeof value === 'string' ? value : value?.aeronef;
return aeronef ? this._filterImat(aeronef as string) : this.inputOptions.aeronefs.slice();
}),
),
imats: this.jumpForm.controls['imat'].valueChanges.pipe(
startWith(''),
map(value => {
const imat = typeof value === 'string' ? value : value?.imat;
return imat ? this._filterImat(imat as string) : this.inputOptions.aeronefs.slice();
}),
),
canopies: this.jumpForm.controls['voile'].valueChanges.pipe(
startWith(''),
map(value => {
const voile = typeof value === 'string' ? value : value?.voile;
return voile ? this._filterTaille(voile as string) : this.inputOptions.canopies.slice();
}),
),
tailles: this.jumpForm.controls['taille'].valueChanges.pipe(
startWith(''),
map(value => {
const taille = typeof value === 'string' ? value : value?.taille.toString();
return taille ? this._filterTaille(taille as string) : this.inputOptions.canopies.slice();
}),
),
dropzones: this.jumpForm.controls['lieu'].valueChanges.pipe(
startWith(''),
map(value => {
const lieu = typeof value === 'string' ? value : value?.lieu;
return lieu ? this._filterOaci(lieu as string) : this.inputOptions.dropzones.slice();
}),
),
oaci: this.jumpForm.controls['oaci'].valueChanges.pipe(
startWith(''),
map(value => {
const oaci = typeof value === 'string' ? value : value?.oaci;
return oaci ? this._filterOaci(oaci as string) : this.inputOptions.dropzones.slice();
}),
),
categories: this.jumpForm.controls['categorie'].valueChanges.pipe(
startWith(''),
map(value => {
const category = typeof value === 'string' ? value : value?.categorie;
return category ? this._filterModule(category as string) : this.inputOptions.categories.slice();
}),
),
modules: this.jumpForm.controls['module'].valueChanges.pipe(
startWith(''),
map(value => {
const module = typeof value === 'string' ? value : value?.module;
return module ? this._filterModule(module as string) : this.inputOptions.categories.slice();
}),
)
};
this._loadAeronefByImat();
this._loadCanopyModelBySize();
this._loadDropZoneByOaci();
this._loadJumpByModule();
}
ngOnDestroy() {
this._aeronefByImat.unsubscribe();
this._canopyModelBySize.unsubscribe();
this._dropZoneByOaci.unsubscribe();
this._jumpByModule.unsubscribe();
}
private _filterImat(imat: string): Array<AeronefByImat> {
const filterValue = imat.toLowerCase();
return this.inputOptions.aeronefs.filter(option => (option.aeronef.toLowerCase().includes(filterValue) || option.imat.toLowerCase().includes(filterValue)));
}
private _filterModule(module: string): Array<JumpByModule> {
const filterValue = module.toLowerCase();
return this.inputOptions.categories.filter(option => (option.categorie.toLowerCase().includes(filterValue) || option.module.toLowerCase().includes(filterValue)));
}
private _filterOaci(oaci: string): Array<DropZoneByOaci> {
const filterValue = oaci.toLowerCase();
return this.inputOptions.dropzones.filter(option => (option.lieu.toLowerCase().includes(filterValue) || option.oaci.toLowerCase().includes(filterValue)));
}
private _filterTaille(taille: string): Array<CanopyModelBySize> {
const filterValue = taille.toLowerCase();
return this.inputOptions.canopies.filter(option => (option.voile.toLowerCase().includes(filterValue) || option.taille.toString().includes(filterValue)));
}
private _loadAeronefByImat(): void {
const aeronefs$ = this._aeronefsService.getAllByImat();
this._aeronefByImat = aeronefs$.subscribe((aggregate: Array<AeronefByImat>) => {
this.inputOptions.aeronefs = aggregate;
});
}
private _loadCanopyModelBySize(): void {
const canopies$ = this._canopiesService.getAllBySizeByModel();
this._canopyModelBySize = canopies$.subscribe((aggregate: Array<CanopyModelBySize>) => {
this.inputOptions.canopies = aggregate;
});
}
private _loadDropZoneByOaci(): void {
const dropzones$ = this._dropZonesService.getAllByOaci();
this._dropZoneByOaci = dropzones$.subscribe((aggregate: Array<DropZoneByOaci>) => {
this.inputOptions.dropzones = aggregate;
});
}
private _loadJumpByModule(): void {
const jumps$ = this._jumpsService.getAllByModule();
this._jumpByModule = jumps$.subscribe((aggregate: Array<JumpByModule>) => {
this.inputOptions.categories = aggregate;
});
}
addSautant(): void {
const tag = this.tagField.value;
if (tag != null && tag.trim() !== '' && this.jump.sautants.indexOf(tag) < 0) {
this.jump.sautants.push(tag);
}
const participants = (this.jump.sautants.length + 1);
this.jumpForm.controls['participants'].setValue(participants);
this.tagField.reset('');
}
closeDialog(): void {
// close the dialog without result
this.dialogRef.close();
}
displayAeronefFn(aeronef: AeronefByImat): string {
let res = '';
if (aeronef) {
if (typeof aeronef === 'string') {
res = aeronef;
} else {
res = aeronef.aeronef ? aeronef.aeronef : '';
}
}
return res;
}
displayImatFn(aeronef: AeronefByImat): string {
let res = '';
if (aeronef) {
if (typeof aeronef === 'string') {
res = aeronef;
} else {
res = aeronef.imat ? aeronef.imat : '';
}
}
return res;
}
displayLieuFn(dropzone: DropZoneByOaci): string {
let res = '';
if (dropzone) {
if (typeof dropzone === 'string') {
res = dropzone;
} else {
res = dropzone.lieu ? dropzone.lieu : '';
}
}
return res;
}
displayOaciFn(dropzone: DropZoneByOaci): string {
let res = '';
if (dropzone) {
if (typeof dropzone === 'string') {
res = dropzone;
} else {
res = dropzone.oaci ? dropzone.oaci : '';
}
}
return res;
}
displayTailleFn(canopy: CanopyModelBySize): string {
let res = '';
if (canopy) {
if (typeof canopy === 'string') {
res = canopy;
} else {
res = canopy.taille ? canopy.taille.toString() : '';
}
}
return res;
}
displayVoileFn(canopy: CanopyModelBySize): string {
let res = '';
if (canopy) {
if (typeof canopy === 'string') {
res = canopy;
} else {
res = canopy.voile ? canopy.voile : '';
}
}
return res;
}
displayCategorieFn(jump: JumpByModule): string {
let res = '';
if (jump) {
if (typeof jump === 'string') {
res = jump;
} else {
res = jump.categorie ? jump.categorie : '';
}
}
return res;
}
displayModuleFn(jump: JumpByModule): string {
let res = '';
if (jump) {
if (typeof jump === 'string') {
res = jump;
} else {
res = jump.module ? jump.module : '';
}
}
return res;
}
getErrorMessage(name: string): string {
if (this.jumpForm.controls[name].errors !== null) {
return `Ce champ est requis.`;
}
return '';
}
onSelAeronef(aeronef: AeronefByImat) {
if (aeronef && aeronef.aeronef) {
this.jumpForm.controls['imat'].setValue(aeronef);
}
}
onSelImat(aeronef: AeronefByImat) {
if (aeronef && aeronef.imat) {
this.jumpForm.controls['aeronef'].setValue(aeronef);
}
}
onSelLieu(dropzone: DropZoneByOaci) {
if (dropzone && dropzone.oaci) {
this.jumpForm.controls['oaci'].setValue(dropzone);
}
}
onSelOaci(dropzone: DropZoneByOaci) {
if (dropzone && dropzone.lieu) {
this.jumpForm.controls['lieu'].setValue(dropzone);
}
}
onSelTaille(canopy: CanopyModelBySize) {
if (canopy && canopy.voile) {
this.jumpForm.controls['voile'].setValue(canopy);
}
}
onSelVoile(canopy: CanopyModelBySize) {
if (canopy && canopy.taille) {
this.jumpForm.controls['taille'].setValue(canopy);
}
}
onSelCategorie(jump: JumpByModule) {
if (jump && jump.categorie) {
this.jumpForm.controls['module'].setValue(jump);
}
}
onSelModule(jump: JumpByModule) {
if (jump && jump.module) {
this.jumpForm.controls['categorie'].setValue(jump);
}
}
removeSautant(index: string): void {
this.jump.sautants = this.jump.sautants.filter((sautant) => sautant !== index);
const participants = (this.jump.sautants.length + 1);
this.jumpForm.controls['participants'].setValue(participants);
}
submitForm(): void {
// update the model
this.updateJump(this.jumpForm.value);
this.jump.lieu = typeof this.jumpForm.controls['lieu'].value === 'string' ? this.jumpForm.controls['lieu'].value : this.jumpForm.controls['lieu'].value.lieu;
this.jump.oaci = typeof this.jumpForm.controls['oaci'].value === 'string' ? this.jumpForm.controls['oaci'].value : this.jumpForm.controls['oaci'].value.oaci;
this.jump.aeronef = typeof this.jumpForm.controls['aeronef'].value === 'string' ? this.jumpForm.controls['aeronef'].value : this.jumpForm.controls['aeronef'].value.aeronef;
this.jump.imat = typeof this.jumpForm.controls['imat'].value === 'string' ? this.jumpForm.controls['imat'].value : this.jumpForm.controls['imat'].value.imat;
this.jump.voile = typeof this.jumpForm.controls['voile'].value === 'string' ? this.jumpForm.controls['voile'].value : this.jumpForm.controls['voile'].value.voile;
this.jump.taille = typeof this.jumpForm.controls['taille'].value === 'string' ? this.jumpForm.controls['taille'].value : this.jumpForm.controls['taille'].value.taille;
this.jump.categorie = typeof this.jumpForm.controls['categorie'].value === 'string' ? this.jumpForm.controls['categorie'].value : this.jumpForm.controls['categorie'].value.categorie;
this.jump.module = typeof this.jumpForm.controls['module'].value === 'string' ? this.jumpForm.controls['module'].value : this.jumpForm.controls['module'].value.module;
// close the dialog with result
this.dialogRef.close(this.jump);
}
updateJump(values: NonNullable<unknown>): void {
Object.assign(this.jump, values);
}
}
@@ -0,0 +1,214 @@
<h2 mat-dialog-title class="bg-navy-light border-bottom border-light-subtle d-flex mb-3 pt-3 pb-2">
Saut n° {{jump.numero}}<span class="flex-spacer"></span>
<mat-icon fontIcon="close" class="pointer" (click)="closeDialog()"></mat-icon>
</h2>
<mat-dialog-content>
<div class="container">
<div class="d-flex border-bottom border-light-subtle text-light-info pb-2 mb-3">
<div class="flex-fill">
<h5>Le {{ jump.date | date: 'dd/MM/yyyy' }} à {{ jump.lieu }} {{ jump.zone ? jump.zone : jump.oaci }} :</h5>
</div>
<span class="flex-spacer"></span>
<div class="align-self-center me-2">
<span class="text-accent fs-5">{{ jump.categorie }}</span>
@if (jump.module) {
<span class="fs-5 mx-2">/</span>
<span class="text-muted fs-5">{{ jump.module }}</span>
}
</div>
</div>
<div class="row">
<div class="col-lg-6 col-md-12">
<dl class="dl-horizontal">
<dt class="text-light-info fw-normal">Aeronef:</dt>
<dd>{{ jump.aeronef }} {{ jump.imat }}</dd>
<dt class="text-light-info fw-normal">Voile:</dt>
<dd>{{ jump.voile }} {{ jump.taille }} ft<sup>2</sup></dd>
@if (jump.programme) {
<dt class="text-light-info fw-normal">Programme:</dt>
<dd>{{ jump.programme }}</dd>
}
</dl>
</div>
<div class="col-lg-6 col-md-12">
<dl class="dl-horizontal">
@if (!jump.x2data) {
<dt class="text-light-info fw-normal">Hauteur:</dt>
<dd>{{ jump.hauteur }} m</dd>
<dt class="text-light-info fw-normal">Déploiement:</dt>
<dd>{{ jump.deploiement }} m</dd>
}
@for (file of jump.files; track file) {
<dt class="text-light-info fw-normal">Fichier {{ file.type }}</dt>
<dd>{{ file.name }}</dd>
}
</dl>
</div>
</div>
<mat-divider></mat-divider>
@if (jump.x2data) {
<div class="row my-3">
<div class="col-sm-4 col-xs-12 text-center">
<h6>Hauteur</h6>
<span class="text-primary fw-medium fs-5">{{ jump.x2data.altitude.exit }} m</span>
</div>
<div class="col-sm-4 col-xs-12 text-center">
<h6 class="d-flex justify-content-center"><mat-icon fontIcon="timer" class="me-1 fs-5"></mat-icon> Durée de la chute</h6>
<span class="text-primary fw-medium fs-5">{{formatDuration(jump.x2data.duration.freeFall)}}</span>
</div>
<div class="col-sm-4 col-xs-12 text-center">
<h6>Déploiement</h6>
<span class="text-primary fw-medium fs-5">{{ jump.x2data.altitude.deployment }} m</span>
</div>
</div>
<div class="row my-4">
<div class="{{ jump.accessoires ? 'col-sm-6 ' : '' }}col-xs-12 text-center">
<h5 class="d-flex justify-content-center mb-4"><mat-icon fontIcon="people" class="me-1"></mat-icon> Participants ({{ jump.sautants.length + 1 }})</h5>
@for (sautant of jump.sautants; track sautant) {
<span class="badge rounded-pill bg-primary mb-2 me-1 pe-3 py-1 fw-normal text-dark">
<mat-icon class="align-middle me-1" fontIcon="person"></mat-icon>
{{ sautant }}
</span>
}
<span class="badge rounded-pill bg-accent mb-2 me-1 pe-3 py-1 fw-normal text-dark">
<mat-icon class="align-middle me-1" fontIcon="person"></mat-icon>
{{ jump.author.username }}
</span>
</div>
@if (jump.accessoires) {
<div class="col-sm-6 col-xs-12 text-center">
<h5 class="d-flex justify-content-center mb-4"><mat-icon fontIcon="auto_awesome" class="me-1"></mat-icon> Accessoires</h5>
<span class="badge rounded-pill bg-navy-light mb-2 me-1 pe-3 py-1 fw-normal">
<mat-icon class="align-middle me-1" fontIcon="auto_awesome"></mat-icon>
{{ jump.accessoires }}
</span>
</div>
}
</div>
<div class="row my-3">
<div class="col-xs-12 text-center">
<h5 class="d-flex justify-content-center mb-3"><mat-icon fontIcon="speed" class="me-1"></mat-icon> Vitesse en chute</h5>
</div>
<div class="col-sm-3 col-xs-12 text-center">
<h6>Max Verticale</h6>
<span class="text-primary fw-medium fs-5">{{ (jump.x2data.speed.freeFall.vertical.max / 1000 * 60 * 60) | number : '1.0-0' }} km/h</span>
</div>
<div class="col-sm-3 col-xs-12 text-center">
<h6>Max Horizontale</h6>
<span class="text-primary fw-medium fs-5">{{ (jump.x2data.speed.freeFall.horizontal.max / 1000 * 60 * 60) | number : '1.0-0' }} km/h</span>
</div>
<div class="col-sm-3 col-xs-12 text-center">
<h6>Moyenne Verticale</h6>
<span class="text-primary fw-medium fs-5">{{ (jump.x2data.speed.freeFall.vertical.avg / 1000 * 60 * 60) | number : '1.0-0' }} km/h</span>
</div>
<div class="col-sm-3 col-xs-12 text-center">
<h6>Moyenne Horizontale</h6>
<span class="text-primary fw-medium fs-5">{{ (jump.x2data.speed.freeFall.horizontal.avg / 1000 * 60 * 60) | number : '1.0-0' }} km/h</span>
</div>
</div>
<mat-divider></mat-divider>
<div class="row mt-4 mb-3">
<div class="col-xs-12 text-center">
<h5 class="d-flex justify-content-center mb-3"><mat-icon fontIcon="speed" class="me-1"></mat-icon> Vitesse sous voile</h5>
</div>
<div class="col-sm-3 col-xs-12 text-center">
<h6>Max Verticale</h6>
<span class="text-primary fw-medium fs-5">{{ (jump.x2data.speed.underCanopy.vertical.max / 1000 * 60 * 60) | number : '1.0-0' }} km/h</span>
</div>
<div class="col-sm-3 col-xs-12 text-center">
<h6>Max Horizontale</h6>
<span class="text-primary fw-medium fs-5">{{ (jump.x2data.speed.underCanopy.horizontal.max / 1000 * 60 * 60) | number : '1.0-0' }} km/h</span>
</div>
<div class="col-sm-3 col-xs-12 text-center">
<h6>Moyenne Verticale</h6>
<span class="text-primary fw-medium fs-5">{{ (jump.x2data.speed.underCanopy.vertical.avg / 1000 * 60 * 60) | number : '1.0-0' }} km/h</span>
</div>
<div class="col-sm-3 col-xs-12 text-center">
<h6>Moyenne Horizontale</h6>
<span class="text-primary fw-medium fs-5">{{ (jump.x2data.speed.underCanopy.horizontal.avg / 1000 * 60 * 60) | number : '1.0-0' }} km/h</span>
</div>
</div>
<mat-divider></mat-divider>
<div class="row mt-3">
<div class="col-xs-12 text-center">
<h5 class="d-flex justify-content-center mb-3"><mat-icon fontIcon="square_foot" class="me-1"></mat-icon> Finesse</h5>
</div>
<div class="col-sm-6 col-xs-12 text-center">
<h6>Finesse en chute</h6>
</div>
<div class="col-sm-6 col-xs-12 text-center">
<h6>Finesse sous voile</h6>
</div>
</div>
<div class="row mb-3">
<div class="col-sm-3 col-xs-12 text-center">
<h6>Finesse Max</h6>
<span class="text-primary fw-medium fs-5">{{ jump.x2data.glideRatio.freeFall.max | number : '1.2-2' }}</span>
</div>
<div class="col-sm-3 col-xs-12 text-center">
<h6>Finesse Moyenne</h6>
<span class="text-primary fw-medium fs-5">{{ jump.x2data.glideRatio.freeFall.avg | number : '1.2-2' }}</span>
</div>
<div class="col-sm-3 col-xs-12 text-center">
<h6>Finesse Max</h6>
<span class="text-primary fw-medium fs-5">{{ jump.x2data.glideRatio.underCanopy.max | number : '1.2-2' }}</span>
</div>
<div class="col-sm-3 col-xs-12 text-center">
<h6>Finesse Moyenne</h6>
<span class="text-primary fw-medium fs-5">{{ jump.x2data.glideRatio.underCanopy.avg | number : '1.2-2' }}</span>
</div>
</div>
<mat-divider></mat-divider>
<div class="row my-3">
<div class="col-xs-12 text-center">
<h5 class="d-flex justify-content-center mb-3"><mat-icon fontIcon="straighten" class="me-1"></mat-icon> Distance</h5>
</div>
<div class="col-sm-3 col-xs-12 text-center">
<h6>Distance au largage</h6>
<span class="text-primary fw-medium fs-5">{{ jump.x2data.distance.exitFromLandingZone| number : '1.0-0' }} m</span>
</div>
<div class="col-sm-3 col-xs-12 text-center">
<h6 class="d-flex justify-content-center"><mat-icon fontIcon="timer" class="me-1 fs-5"></mat-icon> Durée sous voile</h6>
<span class="text-primary fw-medium fs-5">{{formatDuration(jump.x2data.duration.underCanopy)}}</span>
</div>
<div class="col-sm-3 col-xs-12 text-center">
<h6>Distance totale</h6>
<span class="text-primary fw-medium fs-5">{{ jump.x2data.distance.totalFlying | number : '1.0-0' }} m</span>
</div>
<div class="col-sm-3 col-xs-12 text-center">
<h6>Distance horizontale</h6>
<span class="text-primary fw-medium fs-5">{{ jump.x2data.distance.totalHorizontal | number : '1.0-0' }} m</span>
</div>
</div>
<mat-divider></mat-divider>
}
@else {
<div class="row my-4">
<div class="col-xs-12 text-center">
<h5 class="d-flex justify-content-center mb-4"><mat-icon fontIcon="people" class="me-1"></mat-icon> Participants ({{ jump.sautants.length + 1 }})</h5>
@for (sautant of jump.sautants; track sautant) {
<span class="badge rounded-pill bg-primary mb-2 me-1 pe-3 py-1 fw-normal text-dark">
<mat-icon class="align-middle me-1" fontIcon="person"></mat-icon>
{{ sautant }}
</span>
}
<span class="badge rounded-pill bg-accent mb-2 me-1 pe-3 py-1 fw-normal text-dark">
<mat-icon class="align-middle me-1" fontIcon="person"></mat-icon>
{{ jump.author.username }}
</span>
</div>
</div>
<mat-divider></mat-divider>
<div class="d-flex justify-content-center my-3 fs-5"><mat-icon fontIcon="satellite_alt" class="me-2"></mat-icon> Aucune donnée GPS</div>
<mat-divider></mat-divider>
}
</div>
</mat-dialog-content>
<mat-dialog-actions>
<span class="flex-spacer"></span>
<button mat-button (click)="closeDialog()" class="me-1">Fermer</button>
</mat-dialog-actions>
@@ -0,0 +1,37 @@
import { Component, Inject } from '@angular/core';
import { DatePipe, DecimalPipe } from '@angular/common';
import { MatButtonModule } from '@angular/material/button';
import { MatDialogRef, MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';
import { MatDividerModule } from '@angular/material/divider';
import { MatIconModule } from '@angular/material/icon';
import { Jump } from 'src/app/core/models';
@Component({
selector: 'app-jump-view-dialog',
templateUrl: 'jump-view.dialog.html',
standalone: true,
imports: [
DatePipe, DecimalPipe,
MatButtonModule, MatDialogModule,
MatDividerModule, MatIconModule
]
})
export class JumpViewDialogComponent {
constructor(
public dialogRef: MatDialogRef<JumpViewDialogComponent>,
@Inject(MAT_DIALOG_DATA) public jump: Jump
) { }
closeDialog(): void {
// close the dialog without result
this.dialogRef.close();
}
formatDuration(duration: number): string {
const start = new Date(0);
start.setSeconds(duration);
return start.toISOString().substring(14, 19);
}
}
@@ -1,21 +0,0 @@
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { LogbookComponent } from './logbook.component';
import { AuthGuard, AuthResolver } from 'src/app/core/services';
const routes: Routes = [
{
path: '',
component: LogbookComponent,
canActivate: [AuthGuard],
resolve: {
isAuthenticated: AuthResolver
}
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class LogbookRoutingModule { }
@@ -1,7 +1,105 @@
<div class="content"> <div @flyInOut>
<h1>{{title}}</h1> <div class="d-flex">
<!----> <div class="flex-fill">
<huapp-jump-table #jumpTable [refresh]="refreshInterval" [config]="listConfig" [isUser]="isUser" [showAdd]="canModify" [title]="listTitle"> <h1 class="mb-1">{{title}}</h1>
</huapp-jump-table> <span [hidden]="!lastJump.numero" class="me-2 fs-6">
<!----> Dernier saut enregistré : <span class="fs-5 mx-1 text-primary">{{ lastJump.numero }}<sup>ème</sup></span> à <span class="fs-5 mx-1 text-primary">{{ lastJump.lieu }}</span> le <span class="fs-5 mx-1 text-primary">{{ lastJump.date | date: 'dd/MM/yyyy' }}</span>
</span>
</div>
<span class="flex-spacer"></span>
<div class="align-self-center me-2">
<button mat-icon-button (click)="openAddDialog()" aria-label="Ajouter">
<mat-icon fontIcon="add_notes"></mat-icon>
</button>
<button mat-icon-button [matMenuTriggerFor]="menuJump" aria-label="Menu Jump">
<mat-icon fontIcon="more_vert"></mat-icon>
</button>
<mat-menu #menuJump="matMenu">
<button mat-menu-item (click)="openAddDialog()">
<mat-icon fontIcon="add_notes"></mat-icon>
<span>Ajouter un saut</span>
</button>
@for (menuitem of menuItems.getMenuLogbook(); track menuitem) {
@if (menuitem.type === 'link') {
<button mat-menu-item [routerLink]="['/', menuitem.state]">
<mat-icon [fontIcon]="menuitem.icon"></mat-icon>
<span>{{ menuitem.name }}</span>
</button>
}
}
</mat-menu>
</div>
</div>
<mat-divider class="my-3"></mat-divider>
@if (jumpsToHundred() <= 20) {
<div class="d-flex pb-2 pe-2">
<span class="flex-spacer"></span>
<mat-icon fontIcon="warning" color="teal"></mat-icon>
<div class="text-end text-teal align-self-center ms-2">
{{ lastJump.numero + jumpsToHundred() }}<sup>ème</sup> à venir dans {{ jumpsToHundred() }}
{{ jumpsToHundred() > 1 ? 'sauts' : 'saut' }}
</div>
</div>
}
<mat-card>
<mat-card-header class="p-0">
<mat-expansion-panel class="w-100">
<mat-expansion-panel-header class="bg-navy-light">
<mat-panel-title>Filtrer les sauts</mat-panel-title>
@if (jumpsCount < lastJump.numero) {
<mat-panel-description>
{{ jumpsCount }} {{ jumpsCount > 1 ? 'sauts' : 'saut' }} sur {{ lastJump.numero }} correspondent à la recherche
</mat-panel-description>
}
</mat-expansion-panel-header>
<div class="d-flex mt-3 mb-0">
<div class="flex-fill">
<div class="label-container d-flex justify-content-between mx-2">
<label for="yearRange">Année du saut</label>
<label for="yearRange">de <span class="fw-bold">{{listConfig.filters.yearRangeStart}}</span> à <span class="fw-bold">{{listConfig.filters.yearRangeEnd}}</span></label>
</div>
<mat-slider [min]="rangesMinMax.year.start" [max]="rangesMinMax.year.end" step="1" discrete>
<input id="yearRange" matSliderStartThumb [(ngModel)]="listConfig.filters.yearRangeStart">
<input matSliderEndThumb [(ngModel)]="listConfig.filters.yearRangeEnd">
</mat-slider>
</div>
<div class="flex-fill">
<div class="label-container d-flex justify-content-between mx-2">
<label for="numeroRange">Numéro du saut</label>
<label for="numeroRange">de <span class="fw-bold">{{listConfig.filters.numeroRangeStart}}</span> à <span class="fw-bold">{{listConfig.filters.numeroRangeEnd}}</span></label>
</div>
<mat-slider [min]="rangesMinMax.numero.start" [max]="rangesMinMax.numero.end" step="1" discrete>
<input id="numeroRange" matSliderStartThumb [(ngModel)]="listConfig.filters.numeroRangeStart">
<input matSliderEndThumb [(ngModel)]="listConfig.filters.numeroRangeEnd">
</mat-slider>
</div>
<div class="flex-fill">
<div class="label-container d-flex justify-content-between mx-2">
<label for="tailleRange">Taille de la voile</label>
<label for="tailleRange">de <span class="fw-bold">{{listConfig.filters.tailleRangeStart}}</span> ft<sup>2</sup> à <span class="fw-bold">{{listConfig.filters.tailleRangeEnd}}</span> ft<sup>2</sup></label>
</div>
<mat-slider [min]="rangesMinMax.taille.start" [max]="rangesMinMax.taille.end" step="1" discrete>
<input id="tailleRange" matSliderStartThumb [(ngModel)]="listConfig.filters.tailleRangeStart">
<input matSliderEndThumb [(ngModel)]="listConfig.filters.tailleRangeEnd">
</mat-slider>
</div>
<div class="flex-fill">
<div class="label-container d-flex justify-content-between mx-2">
<label for="participantsRange">Taille du groupe</label>
<label for="participantsRange">de <span class="fw-bold">{{listConfig.filters.participantsRangeStart}}</span> à <span class="fw-bold">{{listConfig.filters.participantsRangeEnd}}</span></label>
</div>
<mat-slider [min]="rangesMinMax.participants.start" [max]="rangesMinMax.participants.end" step="1" discrete>
<input id="participantsRange" matSliderStartThumb [(ngModel)]="listConfig.filters.participantsRangeStart">
<input matSliderEndThumb [(ngModel)]="listConfig.filters.participantsRangeEnd">
</mat-slider>
</div>
<span class="flex-spacer"></span>
</div>
</mat-expansion-panel>
</mat-card-header>
<mat-card-content class="mt-2">
<app-jump-table (countChange)="setTotalResults($event)" #jumpTable [config]="listConfig" [isUser]="isUser" [showAdd]="canModify" [title]="title">
</app-jump-table>
</mat-card-content>
</mat-card>
</div> </div>
@@ -0,0 +1,11 @@
.mat-expansion-panel {
border-bottom-right-radius: initial;
border-bottom-left-radius: initial;
}
.mat-mdc-slider {
max-width: 300px;
width: 100%;
}
.label-container {
max-width: 284px;
}
@@ -8,8 +8,8 @@ describe('LogbookComponent', () => {
beforeEach(() => { beforeEach(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [LogbookComponent] imports: [LogbookComponent]
}); });
fixture = TestBed.createComponent(LogbookComponent); fixture = TestBed.createComponent(LogbookComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
fixture.detectChanges(); fixture.detectChanges();
+219 -31
View File
@@ -1,71 +1,194 @@
import { Component, OnInit, OnDestroy } from '@angular/core'; import { CommonModule } from '@angular/common';
import { ActivatedRoute, Router, RouterLink } from '@angular/router'; import { trigger, state, style, animate, transition } from '@angular/animations';
import { Component, AfterContentChecked, OnInit, OnDestroy, computed, signal, Signal } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { ActivatedRoute, Router, RouterModule } from '@angular/router';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatDividerModule } from '@angular/material/divider';
import { MatDialog, MatDialogModule } from '@angular/material/dialog';
import { MatExpansionModule } from '@angular/material/expansion';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { MatMenuModule } from '@angular/material/menu';
import { MatSliderModule } from '@angular/material/slider';
import { MatSnackBar, MatSnackBarConfig, MatSnackBarHorizontalPosition, MatSnackBarModule, MatSnackBarVerticalPosition } from '@angular/material/snack-bar';
import { Observable, Subscription } from 'rxjs'; import { Observable, Subscription } from 'rxjs';
import { take } from 'rxjs/operators'; import { take } from 'rxjs/operators';
import { JumpTableComponent } from 'src/app/components/shared/helpers-jump'; import { JumpAddDialogComponent } from 'src/app/components/logbook/dialogs';
import { Errors, Jump, JumpListConfig, User } from 'src/app/core/models'; import { MenuItems, JumpTableComponent } from 'src/app/components/shared';
import { JumpsService, UserService } from 'src/app/core/services'; import { Errors, Jump, JumpListConfig, Range, User } from 'src/app/core/models';
import { environment } from 'src/environments/environment'; import { JumpsService, JumpTableService, UserService } from 'src/app/core/services';
import data from 'src/jumps.json'; import data from 'src/jumps.json';
//import data from 'src/jumps_02.json';
@Component({ @Component({
standalone: true, standalone: true,
imports: [JumpTableComponent], imports: [
selector: 'huapp-logbook', CommonModule, RouterModule, FormsModule,
MatButtonModule, MatCardModule, MatDividerModule, MatExpansionModule,
MatIconModule, MatInputModule, MatMenuModule, MatDialogModule,
MatSliderModule, MatSnackBarModule,
JumpTableComponent
],
selector: 'app-logbook',
templateUrl: './logbook.component.html', templateUrl: './logbook.component.html',
styleUrls: ['./logbook.component.scss'] styleUrl: './logbook.component.scss',
animations: [
trigger('flyInOut', [
state('in', style({ transform: 'translateY(0)' })),
transition('void => *', [
style({ transform: 'translateY(-100%)' }),
animate(200)
]),
transition('* => void', [
style({ transform: 'translateY(100%)' }),
animate(200)
])
])
]
}) })
export class LogbookComponent implements OnInit, OnDestroy { export class LogbookComponent implements OnInit, OnDestroy, AfterContentChecked {
private _currentUser: Subscription = new Subscription(); private _currentUser: Subscription = new Subscription();
private _data: Subscription = new Subscription(); private _data: Subscription = new Subscription();
errors!: Errors; private _lastjump: Subscription = new Subscription();
title = 'Carnet de saut'; private _lastjump$: Observable<Jump> = new Observable();
listTitle = 'Liste des sauts'; public jumpsCount = 0;
listConfig: JumpListConfig = { type: 'all', filters: {} }; public errors!: Errors;
refreshInterval: number = environment.refresh_interval; public title = 'Carnet de sauts';
isAuthenticated = false; public jump: Jump = {} as Jump;
isUser = true; public lastJump: Jump = {} as Jump;
canModify = false; public currentUser: User = {} as User;
jump: Jump = {} as Jump; public listConfig: JumpListConfig = { type: 'all', filters: {} };
currentUser: User = {} as User; public isAuthenticated = false;
public isUser = true;
public canModify = false;
public isSubmitting = false;
public tableRefresh = false;
public jumpRefresh = false;
public jumpsSinceHundred: Signal<number> = signal(0);
public jumpsToHundred: Signal<number> = signal(100);
public rangesMinMax: {
numero: Range;
taille: Range;
participants: Range;
hauteur: Range;
year: Range;
} = {
numero: {start: 1, end: 100000},
taille: {start: 1, end: 400},
participants: {start: 1, end: 200},
hauteur: {start: 0, end: 10000},
year: {start: 1970, end: 2020}
};
constructor( constructor(
private route: ActivatedRoute, private route: ActivatedRoute,
private router: Router, private router: Router,
private jumpsService: JumpsService, private dialog: MatDialog,
private userService: UserService private snackBar: MatSnackBar,
private _jumpsService: JumpsService,
private _jumpTableService: JumpTableService,
private _userService: UserService,
public menuItems: MenuItems
) { } ) { }
ngOnInit() { ngOnInit() {
//this.importJumps(); //this.importJumps();
const data$: Observable<any> = this.route.data; const data$: Observable<{isAuthenticated: boolean, lastjump: Jump}> = this.route.data as Observable<{isAuthenticated: boolean, lastjump: Jump}>;
this._data = data$.subscribe((data: { isAuthenticated: boolean }) => { this._data = data$.subscribe((data: {isAuthenticated: boolean, lastjump: Jump}) => {
this.isAuthenticated = data.isAuthenticated; this.isAuthenticated = data.isAuthenticated;
if (!this.isAuthenticated) { if (!this.isAuthenticated) {
this.router.navigateByUrl('/login'); this.router.navigateByUrl('/login');
return; return;
} }
const currentUser$: Observable<User> = this.userService.currentUser;
const currentUser$: Observable<User> = this._userService.currentUser;
this._currentUser = currentUser$.subscribe((userData: User) => { this._currentUser = currentUser$.subscribe((userData: User) => {
this.currentUser = userData; this.currentUser = userData;
this.canModify = this.currentUser.role === 'Admin'; this.canModify = this.currentUser.role === 'Admin';
}); });
const currentYear: number = new Date().getFullYear();
this.lastJump = data['lastjump'];
this.rangesMinMax = {
numero: {start: 1, end: data.lastjump.numero},
taille: {start: data.lastjump.taille!, end: 230},
participants: {start: 1, end: 20},
hauteur: {start: 1000, end: 8000},
year: {start: 2018, end: currentYear}
}
this.listConfig.filters.numeroRangeStart = this.rangesMinMax.numero.start;
this.listConfig.filters.numeroRangeEnd = this.rangesMinMax.numero.end;
this.listConfig.filters.tailleRangeStart = this.rangesMinMax.taille.start;
this.listConfig.filters.tailleRangeEnd = this.rangesMinMax.taille.end;
this.listConfig.filters.participantsRangeStart = this.rangesMinMax.participants.start;
this.listConfig.filters.participantsRangeEnd = (this.rangesMinMax.participants.end / 2);
this.listConfig.filters.hauteurRangeStart = this.rangesMinMax.hauteur.start;
this.listConfig.filters.hauteurRangeEnd = this.rangesMinMax.hauteur.end;
this.listConfig.filters.yearRangeStart = this.rangesMinMax.year.start;
this.listConfig.filters.yearRangeEnd = this.rangesMinMax.year.end;
this.jumpsSinceHundred = computed(() => (this.lastJump.numero % 100));
this.jumpsToHundred = computed(() => (100 - this.jumpsSinceHundred()));
/*
this._lastjump$ = this._jumpsService.getLastJump();
this._lastjump = this._lastjump$.subscribe((jump) => {
const currentYear: number = new Date().getFullYear();
this.lastJump = jump;
this.rangesMinMax = {
numero: {start: 1, end: jump.numero},
taille: {start: jump.taille!, end: 230},
participants: {start: 1, end: 20},
hauteur: {start: 1000, end: 8000},
year: {start: 2018, end: currentYear}
}
this.listConfig.filters.numeroRangeStart = this.rangesMinMax.numero.start;
this.listConfig.filters.numeroRangeEnd = this.rangesMinMax.numero.end;
this.listConfig.filters.tailleRangeStart = this.rangesMinMax.taille.start;
this.listConfig.filters.tailleRangeEnd = this.rangesMinMax.taille.end;
this.listConfig.filters.participantsRangeStart = this.rangesMinMax.participants.start;
this.listConfig.filters.participantsRangeEnd = (this.rangesMinMax.participants.end / 2);
this.listConfig.filters.hauteurRangeStart = this.rangesMinMax.hauteur.start;
this.listConfig.filters.hauteurRangeEnd = this.rangesMinMax.hauteur.end;
this.listConfig.filters.yearRangeStart = this.rangesMinMax.year.start;
this.listConfig.filters.yearRangeEnd = this.rangesMinMax.year.end;
this.jumpsSinceHundred = computed(() => (this.lastJump.numero % 100));
this.jumpsToHundred = computed(() => (100 - this.jumpsSinceHundred()));
});
*/
}); });
} }
ngOnDestroy() {
this._data.unsubscribe();
this._currentUser.unsubscribe();
this._lastjump.unsubscribe();
}
ngAfterContentChecked() {
if (this.tableRefresh != this._jumpTableService.tableRefresh) {
this.tableRefresh = this._jumpTableService.tableRefresh;
this._lastjump$ = this._jumpsService.getLastJump();
this._lastjump = this._lastjump$.subscribe((jump) => {
this.lastJump = jump;
});
console.log('lastjump has been updated in logbook component');
}
}
importJumps() { importJumps() {
try { try {
data.forEach((entry) => { data.forEach((entry) => {
Object.assign(this.jump, entry); Object.assign(this.jump, entry);
//console.log(this.jump); this._jumpsService.save(this.jump)
this.jumpsService.save(this.jump)
.pipe(take(1)) .pipe(take(1))
.subscribe({ .subscribe({
next: (jump) => { next: (jump) => {
console.log(`Le saut n°${jump.numero} a été ajouté !`, 'OK'); console.log(`Le saut n°${jump.numero} a été ajouté !`, 'OK');
}, },
error: (err) => { error: (err) => {
console.log(`Le saut n\'a pas été ajouté !`, 'Error', err); console.log(`Le saut n'a pas été ajouté !`, 'Error', err);
this.errors = err; this.errors = err;
} }
}); });
@@ -75,9 +198,74 @@ export class LogbookComponent implements OnInit, OnDestroy {
} }
} }
ngOnDestroy() { openAddDialog(): void {
this._currentUser.unsubscribe(); this.jump = <Jump>{};
this._data.unsubscribe(); this.jump.numero = (this.lastJump.numero + 1);
this.jump.sautants = [];
this.jump.participants = 1;
const dialogRef = this.dialog.open(JumpAddDialogComponent, {
width: '70vw',
data: {jump: this.jump, lastJump: this.lastJump}
});
dialogRef.afterClosed().pipe(take(1))
.subscribe(result => {
if (result != undefined) {
this._onEntry(result);
}
});
}
setTotalResults(value: number): void {
this.jumpsCount = value;
}
private _onEntry(jumps: Array<Jump>): void {
const max: number = jumps.length;
let refresh: boolean = false;
jumps.forEach((jump: Jump, index: number) => {
console.log('Ajout du saut', jump.numero);
Object.assign(this.jump, jump);
if (max == index+1) {
refresh = true;
}
this._saveJump(jump, refresh);
});
}
private _saveJump(jump: Jump, refresh: boolean) {
this._jumpsService.save(jump)
.pipe(take(1))
.subscribe({
next: (jump) => {
this._openSnackBar(`Le saut n°${jump.numero} a été ajouté !`, 'Annuler');
this._resetErrors();
if (refresh) {
this.jumpRefresh = !this.jumpRefresh;
this._jumpTableService.updateJumpRefresh(this.jumpRefresh);
this._lastjump$ = this._jumpsService.getLastJump();
this._lastjump = this._lastjump$.subscribe((jump) => {
this.lastJump = jump;
});
}
},
error: (err) => {
this.errors = err;
this.isSubmitting = false;
}
});
}
private _openSnackBar(content: string, title: string) {
const config: MatSnackBarConfig = {
horizontalPosition: <MatSnackBarHorizontalPosition>'end',
verticalPosition: <MatSnackBarVerticalPosition>'bottom',
duration: 4000
};
this.snackBar.open(content, title, config);
}
private _resetErrors(): void {
this.errors = { errors: {} };
} }
} }
@@ -1,15 +0,0 @@
import { NgModule } from '@angular/core';
import { AuthResolver } from 'src/app/core/services';
import { LogbookComponent } from './logbook.component';
import { LogbookRoutingModule } from './logbook-routing.module';
@NgModule({
imports: [
LogbookRoutingModule,
LogbookComponent
],
exports: [],
providers: [AuthResolver]
})
export class LogbookModule { }
@@ -1,25 +0,0 @@
import { Injectable, } from '@angular/core';
import { ActivatedRouteSnapshot, Resolve, Router, RouterStateSnapshot } from '@angular/router';
import { Observable } from 'rxjs';
import { Profile } from 'src/app/core/models';
import { ProfilesService } from 'src/app/core/services';
import { catchError } from 'rxjs/operators';
@Injectable()
export class ProfileResolver implements Resolve<Profile> {
constructor(
private profilesService: ProfilesService,
private router: Router
) { }
resolve(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): Observable<any> {
return this.profilesService.get(route.params['username'])
.pipe(catchError(() => this.router.navigateByUrl('/')));
}
}
@@ -1,22 +0,0 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { ProfileResolver } from './profile-resolver.service';
import { ProfileComponent } from './profile.component';
import { AuthGuard } from 'src/app/core/services';
const routes: Routes = [
{
path: ':username',
component: ProfileComponent,
canActivate: [AuthGuard],
resolve: {
profile: ProfileResolver
}
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class ProfileRoutingModule { }
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,26 @@
/* Profile */
@use 'variable' as var;
.banner {
background-color: var.$sidebar-footer;
padding: 1.6rem 0 1.3rem 0;
margin: -1.5rem -1.5rem 1.5rem -1.5rem;
.user-img {
width: var.$user-img-size;
height: var.$user-img-size;
border-radius: var.$user-img-size;
}
h4 {
font-weight: 500;
margin-bottom: 0.75rem;
}
p {
margin: 0 auto .5rem;
color: var.$muted;
max-width: 450px;
font-weight: 300;
}
}
.mat-mdc-card + .mat-mdc-card {
margin-top: 10px;
}
@@ -8,8 +8,8 @@ describe('ProfileComponent', () => {
beforeEach(() => { beforeEach(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [ProfileComponent] imports: [ProfileComponent]
}); });
fixture = TestBed.createComponent(ProfileComponent); fixture = TestBed.createComponent(ProfileComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
fixture.detectChanges(); fixture.detectChanges();
@@ -1,23 +1,54 @@
import { Component, OnInit, OnDestroy } from '@angular/core'; import { Component, OnInit, OnDestroy } from '@angular/core';
import { Title } from '@angular/platform-browser';
import { RouterLink } from '@angular/router';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatDividerModule } from '@angular/material/divider';
import { MatIconModule } from '@angular/material/icon';
import { Subscription } from 'rxjs';
import { Errors, User } from 'src/app/core/models';
import { UserService } from 'src/app/core/services';
import { ListErrorsComponent } from 'src/app/components/shared';
@Component({ @Component({
standalone: true, standalone: true,
imports: [], imports: [
selector: 'huapp-profile', RouterLink,
MatButtonModule, MatCardModule, MatDividerModule, MatIconModule,
ListErrorsComponent
],
selector: 'app-profile',
templateUrl: './profile.component.html', templateUrl: './profile.component.html',
styleUrls: ['./profile.component.scss'] styleUrl: './profile.component.scss'
}) })
export class ProfileComponent implements OnInit, OnDestroy { export class ProfileComponent implements OnInit, OnDestroy {
private _user: Subscription = new Subscription();
title = 'Mon compte'; title = 'Mon compte';
bannerTitle = '';
user: Partial<User> = {} as Partial<User>;
errors: Errors = { errors: {} };
btnSettingsTitle = 'Paramètres';
btnPasswordTitle = 'Changer le mot de passe';
constructor() { } constructor(
private titleService: Title,
private userService: UserService
) { }
ngOnInit() { ngOnInit() {
// this.titleService.setTitle(`Head Up - ${this.title}`);
Object.assign(this.user, this.userService.getCurrentUser());
if (this.user.username) {
this.bannerTitle = `@${this.user.username}`;
} else {
this.bannerTitle = `${this.user.firstname} ${this.user.lastname}`;
}
} }
ngOnDestroy() { ngOnDestroy() {
//this._scavenger.unsubscribe(); this._user.unsubscribe();
} }
} }
@@ -1,17 +0,0 @@
import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { ProfileComponent } from './profile.component';
import { ProfileResolver } from './profile-resolver.service';
import { ProfileRoutingModule } from './profile-routing.module';
@NgModule({
imports: [
ProfileRoutingModule,
ProfileComponent
],
providers: [
ProfileResolver
],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class ProfileModule { }
@@ -0,0 +1,506 @@
[{
"_id": {
"$oid": "646676e011a65b214af84215"
},
"favorites": [
{
"$oid": "6468e67d3a6bccf2fe57fb18"
},
{
"$oid": "6468e67d3a6bccf2fe57fb1a"
},
{
"$oid": "6468e67d3a6bccf2fe57fb15"
},
{
"$oid": "6468e67d3a6bccf2fe57fb24"
}
],
"following": [],
"username": "jgautier",
"email": "jgautier@aucoffre.com",
"firstname": "Julien",
"lastname": "Gautier",
"phone": "0661655763",
"society": "AuCoffre",
"salt": "ddaa921b00889b72c53a443f379022f9",
"hash": "0b767756b834d313c9f2d2addbc16a1c42ee15385d65c73ddbaa474da66239bbe77cbfbaf40f53a127bfedfd81403cbc252902ff6ac8f16b7d70fdfa1d20c98aab3b367dbeebbb76d9e74fdabbbbb06fda9b14b60add027042d5b582b2d259f58024069da79b018449bcf752c7ac8faa0580f0b85c14a0e49883bd4e5eebcf1803731997d7c5d2216ea80fb4463021483d5e8d196ccddcdc3b51bb8cbb85f563a2f51b616385a50fbbb5b7fb1bd4c25cae4b81eb7ccde0d89e302782590a26e2f769a5903d4649076f3df15f2a9c60a0180d143702b0259c090b52432fcfca4693a3b79d09d00f820c9ecc53984c41ea215ae032d89e4c089e7dddbfdf2352e6d429f3c896fe2b2c6a671f6fcd73134e8ebb082d3060aaa36cc40e6d5f24236b13d3643aeb4d03fad11cce434c95cf4f812186a9093117e3f3a57a978702f3a86beedea0aafbb1d0b04db6de42151f80a3c86dd55705e5ab8b70ca6fc2321e9960076674b93c097451bc0834a4be2e7f69881296c75432fd13127bbf6efc2fbcd4a6752164f03ba8079d30715b2d5a1d6f0d4ffa8670c593a2c96791e621617f139ad985f0843763691f6e6348ea6d4e07cadac5680b7037b30511372230cb65aa3c36cb4ea8154e852ea76da853c9b090a5e0e6f0e3b9fe568f35dc0b6902d75ae7d95a34f040f69add055fb375be86de1498f362e1265e38cfa20e402b7e80",
"role": "Admin",
"createdAt": {
"$date": "2023-05-18T19:05:04.978Z"
},
"updatedAt": {
"$date": "2023-08-23T14:12:49.144Z"
},
"__v": 83,
"image": "/assets/images/user_avatar.jpg",
"pseudoAC": "LEPARA"
},
{
"_id": {
"$oid": "64681bf16ff7b20feae78554"
},
"favorites": [
{
"$oid": "6469770267a3086eb994c62c"
},
{
"$oid": "6468e67d3a6bccf2fe57fb1a"
},
{
"$oid": "6468e67d3a6bccf2fe57fb32"
},
{
"$oid": "6468e67d3a6bccf2fe57fb48"
},
{
"$oid": "646b78739cc692a8a4eb9970"
},
{
"$oid": "6468e67d3a6bccf2fe57fb36"
},
{
"$oid": "6468e67d3a6bccf2fe57fb38"
},
{
"$oid": "6468e67d3a6bccf2fe57fb16"
}
],
"following": [],
"username": "julien_gautier",
"email": "rampeur@gmail.com",
"firstname": "Julien",
"lastname": "Gautier",
"phone": "0661655763",
"society": "AuCoffre",
"salt": "4c87e9e9f899d03593faa1eb9e903bda",
"hash": "e27251923c34a0bb886d8d21b2de3fc784002071865b4e69b4c8b1c3adca1bc1ed68e7387453612abdffaa9f24476d81e621d450346a77f6303c8fc2e69ad74a6a795de7f12b0520772a37cc70af166920beac1e57e7f837b4c995016e00e44e1638da5916b209ceab543c8f58bdc2ada8d3dbb6d91630f3923a0901719432410c040fd379ded1f2ed7cbc2015b36ae1b31357b99e6aec9e1179a74dbd2620684f66d6ed3d117144349277d488effd7ea897c6040b3a0c8068ba982a155c3b1d2c8f063e6e8c8590b2b0e8c489b28b48503ffd35c99f54b59f58522319abc81a1e5197ba0f6d2cbbb2affda309e87db48aa76228418ec6875a8f678c35e805ebbfe54a2bc23c429153a4c4e18aedd6b74cf61a237899a9c94c104a69f04323cce32c95890c5cbae84415dc44068072fb7bec14379d61b77c084f967f42de331421d31e36ab423925cce28ddcbd770223936c026af7d8a5ecbf6777f52845e9e85ed67e358fbe591f68231e8f658b140bcda358faa4dd028b1048679900314ec75fdfb5f64c96dd6abc39e3c60d22c67500da9883e2dedb508175a8cb4f66091ea142b3c3a4fa5e9784875a9e0a1aff659ddf8bdccaf1dc3524a5a83c094333afd29e6280bc7af93d0aa6f593b40f49c29739940a412aa0364bb21d6463318826267e4cd4825ca4baa7e1a526d62ceeadcf8d73969e0c490c3588f476058eb4e0",
"role": "User",
"createdAt": {
"$date": "2023-05-20T01:01:37.633Z"
},
"updatedAt": {
"$date": "2023-08-01T08:40:21.130Z"
},
"__v": 70,
"image": "/assets/images/logo_user_002.jpg"
},
{
"_id": {
"$oid": "64ba8c824ecc6ac7bb7629a3"
},
"favorites": [
{
"$oid": "646921ceae37e3f3574d9ab4"
},
{
"$oid": "6468e67d3a6bccf2fe57fb15"
},
{
"$oid": "647739a22ee14fec66329870"
}
],
"following": [],
"username": "abusco",
"email": "abusco@aucoffre.com",
"firstname": "Anthony",
"lastname": "Busco",
"phone": "0631902525",
"society": "AuCoffre",
"salt": "b8e33b5ae7ca938ab466ba80fdae6e5f",
"hash": "37ee30a899b7fa6596555d2b538ca89ee5af42e7fbc9de0bea82657b0561c8dc747ffcd383776b97e56fc40513098a2860b13646b9112b1c0f0b086d718ae881e313153b0947b807485169931ed4ff99e8d70df7e0481b6a13f83cc2750dc3e1b3fde0e8a5de5cb939647250b677e1f5958c6a0cc8c64ebceffa009c13ee89b1c4acdba9e4b4dde8fd08201a813e320317806716adcd984959c29ef21e67b4acbc0f2a54f606a469a7e4708bd60b75229af03972a14eb1dd5c62533428ee1acb2d1e4e66d045689f5a4d31ddaeccebe2e9254dd87417490378ae568c0ff16708063de56754e3550ba1c6be498773b0e8fd5c260a8e9e97b284db8ec2694ccdd9290dba83111d1c894af57bc70ac11337b2df0cdcd3c44e2adbea2939b70ee3402c0bcdcbe13ca220406422e3ef43f1ba8e57018d461822adc234b44430db8079ddf9b32d926ac82db9a23169b79ada4482c7bf14e6b1a4f7a38840e578d0307f9acaae9d6c373f65415e6b5dbcdda94bf40331f7c39c9eb07da1a1f18ee7d2eae8773e8aa76c9cd822bf25daefbefcdc4d8d1576197cf5dbb2c5677f2a624b5aac549cccb53a6d9b4c37243dd865f150e042c798ab1904dc704bc62853253a7ca8a58f79c5b607b043a9a5efed2e86c8380c1fbdc3227b5c0288f49ad7cb005c015bf1785caaee41fdf37fdfcb6a5e35789339a36ed6d31fe5bcccecec04b9b3",
"role": "Admin",
"createdAt": {
"$date": "2023-05-18T19:05:04.978Z"
},
"updatedAt": {
"$date": "2023-07-21T13:49:08.539Z"
},
"__v": 1,
"image": "/assets/images/logo_user_001.jpg"
},
{
"_id": {
"$oid": "64d35f07f42bebe5d1625fef"
},
"favorites": [],
"following": [],
"username": "christophe_marchesseau",
"email": "info@oretcie.fr",
"firstname": "Christophe",
"lastname": "Marchesseau",
"phone": "0671563041",
"society": "Or Et Compagnie Eysines/Villenave/Fargue",
"pseudoAC": "WYLERA",
"salt": "10b786c0f3500268f59b72eeac903d00",
"hash": "57d9a1600146152d274749f2a9237d3ca33eb6c2265ee2f89f557ce375a91f1699a14c0a57145d12cab55b96d5cea995a1a87ba768ee01c7717c6a622580c64719d2cffba1b6027ca27cfcdd1cdc2ddeeaa91fc5b6b533b89c453723d68a5611f2c01b23bc32860834b28e158984a96bcecd05c990a3f6ed0e3111f9619375f6a16e52c02999d3b58f815be115e9e49f30c54bf6b2416287b7cb46dbb90e1634ba545637b565eda680b543d041d41dcd0c79b8c32f2bc40446d634ba065ff44a5a2dd1e21cb1144066ff34a08d03b00e94b75c5c94f81cab9f352fc9b7456e5ce2741fccee92a7835dc4d117cabecf1307978c9e1507541245d4e629a251e730ad388a1bb42e06a3a0478f50914289a96bc226375cab8a61b6f840979d75b57a0821174914ae931805dea02af37c7d2f5a2cc75578ad4f4ea4684fb0047dbc2463f0164a543da271f8b3b743ed1d981af05d230398b9f4246d7030c983db81628639373d612bc9b49de703b7de84de812914f1214d958e3b737600a4f5a8ee0ec6cb4c03c4b96fba6d0f1ba58ec23880d16a9df072b8cf96df2430e183caf0f16955c409913adda2fad8942178a77fb31d6273b9bf49f24a219a2b7c6897cccad8d5d6e24a2b2e342bcfc510a74f9f43539cb69380e3a9bdb3da03169814aca1d37a942e2951ef9e1a13ee865ba1aabb6a2083f6c609a659fa242c17b1d82afb",
"role": "User",
"createdAt": {
"$date": "2023-08-09T09:40:23.133Z"
},
"updatedAt": {
"$date": "2023-08-09T09:40:23.133Z"
},
"__v": 0
},
{
"_id": {
"$oid": "64d35f98f42bebe5d1626001"
},
"favorites": [],
"following": [],
"username": "joel_hourcourigaray",
"email": "oap33170@outlook.fr",
"firstname": "Joel",
"lastname": "Hourcourigaray",
"phone": "0619446471",
"society": "Au Napoléon d'Or Gradignan",
"pseudoAC": "JECUPU",
"salt": "1078ee75bad68b2845bfae3c8ab2df4f",
"hash": "4135d652f196e75d4e1a8e975ae559b05a65d05feaabf373a6844656437c03a6e7756dd0be696859bf8c877037aae1b41ac9919f84dd3ff55549a6de4ae5a9972662c668e163e1c97d182ca941f4821f99d0cb4c3e95b887793cf20b282ce491d55aad31e6f381a14f0919274b26122d7e548808276fbb003d03bcafbdc3315022d2047b2ce76f93cc324c1e5f3e30fb278d1f68b507469a35f43d0f416894f090a0b9ef1dbe7583be438b17c336f8728178437a24fa39d4e4bcd19b27c2c9ce24647575c3e1dca9d4488717901e555f920cfaf1cbe1761f9609747369f376ce135e9dfeb7439e66cd02ef66e62f59e96ab5c93292361bc8f2629ed35627f6c851b2f4c75154ec3246e1b2ed1f8a8b7746ca77014ce1cf1973a6059612dc108dde2bc1d46801e877a7690b24674f69f5e6df5361028966d7d2cc814afc44e1b661d9996c0b8c26cfe5a8d0f63a9be397cf37ecbcf541ab3ab296b4dc14ac8516aa929590d0d1110670182d3b998e0b6ec6b26cffbafd255dea6a738b65b3145df55aafbf983044ded11b2606517888af56665a2eeffbf767d1fb75cde2fe1d714ec15badab4e9bc57e963991c047cf997959255cd6d2e8ac4755afd28a1a393ebafbe3397a0493ed23f1c4c73e5ecd4cff07b664cf880d430409503801888d77b7afd1bf3e5e5ef83f92d9169c4bf9e751aed7f3adb2305d6dce60f7a8ab8bb2",
"role": "User",
"createdAt": {
"$date": "2023-08-09T09:42:48.377Z"
},
"updatedAt": {
"$date": "2023-08-09T09:42:48.377Z"
},
"__v": 0
},
{
"_id": {
"$oid": "64d36004f42bebe5d1626013"
},
"favorites": [],
"following": [],
"username": "valentin_darteyre",
"email": "aunapoleondor@outlook.fr",
"firstname": "Valentin",
"lastname": "Darteyre",
"phone": "0637848630",
"society": "Au Napoléon d'Or Langon",
"pseudoAC": "UMIFER",
"salt": "047815ff4983d8848ab14aa9af6ca054",
"hash": "d0640f88e03e20e9b7fe84970226dde3346f6ce139a714b7b63e5d2c0dff3c60e6a36c2856f94c85578a8bf2f4410eb418ca28e9bad23e7cd45c583a79128d188a9d92b020f0c43175f94bf7c5fa72a65ac94d9b59bc558807177a3013167b4e3379305bab32808a2d64264c78950d242ff8ac6524cf08bc7d8ff28c97e79cea62cb414a04e1c7aa838a1d29121a92f4d574c45058352aa5c632384dff05f0f00b57295795bc2d2ae62612fe0b3ede0d0ed7860da86ad8eba5fa09a00bb0f7bba911a9659f8f8980946a5068f2e1077c864ba21b36d01f299269f4faf24ccedf2fc005f9e62ce7d1fbf2b322415fb9f0f3cf6c3f9edff1a2d45aefee8ae631a8dda425b8d5285637c96ae57911fdbf7179e6a5eed40dea89e1836545c1e1247734556a4d78ce69f65ab8a74ddc4a7ed3d5688afa1192b6383ceac427e1f79e99ec3db2dc700f847a9393e17f5aa9a267ce4a649f32d623f010e7d218b7c99624aacd2239760735c83e6f9f01ce38474aa7e07feb69aea69304725d8e9bfb9c8c7d0000a84a03cb4efa82629052437c352f27bdd356486eb9c498e9759abb15dfd6405b0b301913991dec68ae5ae4e649e30f738f535bbd95f9f4511e171eb978eae47b542c05156a9b7fcfa7987497a31a99538044bbba933d58cae7880da132dd083e2ca24c060be090608138e49893cbdf1b61b672046dc5a2acb30d8b49ac",
"role": "User",
"createdAt": {
"$date": "2023-08-09T09:44:36.505Z"
},
"updatedAt": {
"$date": "2023-08-09T09:44:36.505Z"
},
"__v": 0
},
{
"_id": {
"$oid": "64d36063f42bebe5d162602b"
},
"favorites": [],
"following": [],
"username": "johann_abecassis",
"email": "or-vintage@tutanota.com",
"firstname": "Johann",
"lastname": "Abecassis",
"phone": "0623196292",
"society": "Or Vintage",
"pseudoAC": "ZUDYXE",
"salt": "2f8976c3e7653ecfb64102303e8717bb",
"hash": "3fa4e42ecff2d0a827e42667e59d6c75db79c7e4c8e4fb69028f46c6160d63255314a3878e60a7ba11b603d0be74ef786f241ab324a5d31852beaecead19843e753cdf45ff74ff25a0839ab5405807656633ab2a244c8d8e643e4eb8dc81e2b09ded66fdbc93867a12c110dc0a96de01702f88ce4d99521ed6974dfe483467c352110ec1b4f675cf4aba2094e9b755f897e8cf5cc6b45558663df91593bf5da889d37a81c4129e522576e2facd28513decd1a4c01fa879f75d33fbc315f70c8b8f2dc8fd2125dadd95653c6d266189a2e1624fcd3a333ed798b7f24b676f1493a8feba627334979a7ecadda07a3d13d0baa98499a795181c1b1b6233d6ac22bb7cc3ce19f069a72019ed7d27ad3ce01be0c308885bcd7685aa8eabcc2393e1a6b85ae883972eb662ba660f469d16663b728c3a3b3f6e0ae0eef92e08f205f63ed8bf86054766d3095cfeed1f462d66f826a71dd453db52c4212a70b319edc19c4b329b901b9fa5ffbf4a20e691ec3981d781ed94bafbc646443850d76629b346aa60f5b480a82ff79d487fb7462f33b9b9534baf1970027a8b6f5994b7655c00b5ee5bf0c2311b8bb1688167c5fef786b0e1cdf93f0742f3a6093272cea551312fb5ef431fb70a61f69a647205195094faddccbe55cb27f9be4b32c3fee326271fcf8fa586a1ebc65b43f00a23dabdb324ff3277f679ba9bd6d0dee920a51126",
"role": "User",
"createdAt": {
"$date": "2023-08-09T09:46:11.647Z"
},
"updatedAt": {
"$date": "2023-08-09T09:46:11.647Z"
},
"__v": 0
},
{
"_id": {
"$oid": "64d360b0f42bebe5d1626063"
},
"favorites": [],
"following": [],
"username": "murat_aydin",
"email": "eliyor4430@gmail.com",
"firstname": "Murat",
"lastname": "Aydin",
"phone": "+32499344802",
"society": "Eliyor (Belgique)",
"pseudoAC": "FEZABA",
"salt": "0137a9f77588bba98b10bb759b471045",
"hash": "0a4ebdf614963e9a1deb53499faa716a1895ff228f763b799828f7fe1917bf1625f5c95b88ee103f9516d129ca8bfbcf821c18977ce583e293e02e210aa97f4239d10a30a36ef0af69377b0e5c8d565ecd64597774d0064b6855713703b12536ae631f333db68a108aab426c8888561a88dca6ddafcf7b0c61c6d76c0c623b18ba7c72fd380cefa83aacf37a9c16dbb866f8a86b933966b86145623b2cfda034a7e257f4847d7ae982cd439cb6ccbfd5b61856247819aa2de75a55cf308a0e0832d7fa94bdfee85bc883bb47020f16ff12d13882216bb4adb27bcf44c704454bddd0a200961b0acd8eb5e41a7afc127c678399dbe2b3a0677d6e81330669babfac481a3fde4db59e0e4bd36f8bc6deb01bbfa375959f666b5108acab37c40590bfcd7ea4093c2e04d7ba0c51b295d119813185abdf6b29e5c05b6b47ccc60a9661b997d753b9350c9e00e95d5a33398ba5bae3c5365f7d19699650001f19f70da874db812149d53d93d13ae8f51bb0af39fd23bd85f5d31e1c2362ff8e38c554dce4e263f70407f09d7f67eb2ac032da854232ff0329e16f375c322c3b1b9e1d8776af6cb2d0ed6cc6a81aa16c9cc602757a3447ebe684cbc2c986cf6921f289021f22732845928e006c8a96d3641fdffde9b4994bf77422667e9cb2e8f142f89e4b6376dbc4801a10d26cad368a22bab92d2ac30f5b0f19e61bba14af0c6e49",
"role": "User",
"createdAt": {
"$date": "2023-08-09T09:47:28.244Z"
},
"updatedAt": {
"$date": "2023-08-09T09:47:28.244Z"
},
"__v": 0
},
{
"_id": {
"$oid": "64d360def42bebe5d1626075"
},
"favorites": [],
"following": [],
"username": "eric_buichon",
"email": "eric.buichon@orange.fr",
"firstname": "Eric",
"lastname": "Buichon",
"phone": "0683460137",
"society": "ERBB / Aureus",
"pseudoAC": "",
"salt": "bd93626a314b3fb1a87c5a0ebd4bec60",
"hash": "7660f16bac71d7c26645aa2cf050423c9e63d4ea3150ac383f54ea66268e12c8d1f3ab2263fae6f1b9a82961f4ac9dd5e24e6f4c6be5ac980ea3cb670448585c5f9a20c042d89e6c3353475d5ab190f273a032e9c7afe00fc9173ba9d9c4bada0eb862081c81e3d875ec36754b19d9581333c3d228cc1cb26add38b23a468b5d0ba524c3ecbfb338c4e4d07a8461fc98750c8afbc8ebbf7a672c7d188d078cb095751818250f93a6212fe9c1bb0201c19348f2e1fd56bc7e7f3df453be3b7b4762580c8c1a63d953ca79ed40b40ebc50aed1e338025952e54ecd9973f10336d2a93a771536c588b54727b70f31d435e9409075bf59246e46d7525fbea2371b05d3e93d8018ae0be1d5e6a925cd2160ac4d1a81f857c4b5a9f893f5fac93e00e52c07d7f0fe97c92e573c50fea3f7a01468bfddc3499a23dd4d59f1bfea6c0a6c01cebdb2beb06f14d128de067c1d8b1b876747701182c4946a05b78424e4a6953448ca676e7fb7db48144bd7958d0a491b06df92d7b44fa8c5906845999711c68fd4bb799f03d9f64dd3081ff8f564214c36846f446f74dd65c7a473bdb8ba5aea738b59fd60d69cbd62dd71dac41a2c55c0a372372cb5222b8ca932233f712908de4bd0b92e2b045aaaf9f9084d4a537ab5b90b2fe752e7fc16c18392daeb90a4378d9c8bc56280e1164c045cd9d7393b36506f7fa0943c19d0a131c384a693",
"role": "User",
"createdAt": {
"$date": "2023-08-09T09:48:14.532Z"
},
"updatedAt": {
"$date": "2023-08-09T09:48:14.532Z"
},
"__v": 0
},
{
"_id": {
"$oid": "64d3610bf42bebe5d1626086"
},
"favorites": [],
"following": [],
"username": "sonia_yungcker",
"email": "contact@numissima.com",
"firstname": "Sonia",
"lastname": "Yungcker",
"phone": "0682751820",
"society": "Numissima",
"pseudoAC": "",
"salt": "dd5ee0f64b1e69664ceaac107c56668f",
"hash": "0efd56217b6a19fa841e7997bd34c3913df55a46d6310d44731bfc60f80ee0247503783ddc7a61adb14a9c71ccdaf6b02bce9f4b454c170a1b605bc21bcb30ad14886008193adf6b4f422e827f12f75d123aa8fec658b22de3cc0ed9331452a631af8d8a431a98bfc5310788dd4a2b58d36471f6608dc08f085eaf0f223a01e47738aad20c271fbd8575d6e22b82a6932749538b7dacd3b547634ed692df7a54d8571f8db24914f17954845de46df658a63c4cd25fca892b581c6cf5ae8f1871d9e36f3997dcb7190e284314331c018f573eb6bb72597e6406f1a88f5ec6d29d5288b8b8aa4a7d0a0184463e51b65dae5d37ced74939d4c1ea37c767e0f92a0cf8e6d990ad33abd331593d2722bfcf7276e5d55b879cfe726f759865151a3bd1f9c43e77aa09da765b05b6faebc512bb6071da5f66e4057c1d9161994769d53c3fd3c98637fc11a4d6b3a4e1263b9978b62a90761efbd4f6bc431c43f91eee6df1c9df1b259c6f232db794ed02b83d5d669965608dd6554a6f4e5613a6347520065d56d73e5184cf6a446ab13e652513fce973f8055642a547c3bfc0ca48652e8c030389203fb83d37358b0f1542900adf1c27c68fb9aaf7455ebf78d9f2419a9c6a264291ff06883025f78e1dccf4d1e7ed57893d601eec5a19b166c6496f8f4a770b3b78172f4c7fb5727d9dda7399f3c7b68af7f734a0109f0eadcceb7ae8",
"role": "User",
"createdAt": {
"$date": "2023-08-09T09:48:59.923Z"
},
"updatedAt": {
"$date": "2023-08-09T09:48:59.923Z"
},
"__v": 0
},
{
"_id": {
"$oid": "64d36205f42bebe5d162609d"
},
"favorites": [],
"following": [],
"username": "mohamed_hamza",
"email": "cbbo.33@wanadoo.fr",
"firstname": "Mohamed",
"lastname": "Hamza",
"phone": "0643358437",
"society": "CBBO",
"pseudoAC": "",
"salt": "e3d8602fae3ded82fa80a67b2b784630",
"hash": "d2a0de718d8d2766130482264b3236af7612dd12607cb5bd1717c12b7ee6dea3f40c62ea3e5c7b1d537b5e47a536ca3a20b7d5805854384199d1249687be06f2d123f54b55cba52de343142287820a6d80786353bb8968a01e2bd8a7d7cbd62844fe710b85b837e0e272c02ddb634fb65c390f149cc0e75515ee69297c7a09dd3454e1e841931c400be0368271d85f3718714fc434bd35adf0503f040baeabf6de747ce54da2291aca4b5b75288e7e3d9d78bb3b0cf6486dfc01eeb23c3011f2fd0002fca3b9d227987dac6ff483d4e27da0ad42224a1b0b0e31a0c34b9a0e62c8c2cca00ff52391d8ac763e02bc88260dbe28d45b1f200494ab155d294021ade4e4829b708540371cc640d3d305d237067b81b130d49a8eea377f3aa5f69ce1d51ff7529d5e12f67d2b1b00e0efe6e5afb647e8a4e36863e9517b83ddddc93bd63392d385831edba06a870621cfed6f525eec80be8532ffef4b6026a39c90c68aadebb9417d3a4f9e84a31fe419e4a3af2301e559d9bc249c43c0c1c1b9db594dc36c542e5316868bc22c2a34062aeeb2c6049d8fd1d66796a88c7e679f5cd0a811e5a3c3626b7a6ec1d52158869696a22b8293c950f2d77d2a0324715386dfa50edd83f1e170520fea11e0a7ef04651523610ad984977542729206c709fe12a75c33535015d72d4ab08e832a335d3df8833fd6f9f833f9c4b960eec980a552",
"role": "User",
"createdAt": {
"$date": "2023-08-09T09:53:09.330Z"
},
"updatedAt": {
"$date": "2023-08-09T09:53:09.330Z"
},
"__v": 0
},
{
"_id": {
"$oid": "64d36234f42bebe5d16260af"
},
"favorites": [],
"following": [],
"username": "bruno_abitbol",
"email": "info@artetorbordeaux.fr",
"firstname": "Bruno",
"lastname": "Abitbol",
"phone": "0608994303",
"society": "Art et Or",
"pseudoAC": "",
"salt": "7ac0adf3e8cd029bffdae7972e1285f0",
"hash": "aa4b30596df6d9ff74c9412587a469b6a90316bd7cf59f1d9a032f23e95a62cf8c73f1e3f97ad401bea1a74e3b987861fb929e99022bbff4ad18c3592412b32ebd7e0b387b4319060a624486488d9760cf98a306d49de61da87c26ad8640b3e297946663a7c04e7f2b7b6c015f601e1c7f180d7cade3e0a4e9f039d92f1cbf4f9ddea521aa638fd2d80c1cd6ed2dc8e085122446f14951a78728698ba67871746739cc330ab820f8200cb8be43f88ff2a8fbccaa60000c629af4bbf1251fac444169d92139d65e372ab946d46398e90976328bb55e4f5e741824614b50c8dbea7e67026983966d7bc5d2a10bb4b32cc2ba608557fb80efea74fb3dc5a08101087f08a1c35d3d5816d3065d79c9f46c55f761c49daeeac389f1ccb867a3416df29516524765ecdcfe39bb5fd7cc5b69c186d850c7ce25d1264445d368a02bcbfb1f822cd5335b99e1ff54fcded3c65a2ee3251fee30c02b5041c6f96a019c3107c327f9e170c090f23d0ed8412521ff1cbb2f0525612618024361394bd97c0b9488a66ac77bae86ebda0507b2994a0431284ddcd9543cb1d95b6ef0fb64fa3fead41d2d9f9bf818117dcd6f28578071460bb8e3d917ef73836c234f34d39f73d988c03647ed476ba8ce251d52a767b0790ff6596a0d0f6390d64c73d14b6100361661d3b10dfd56dc0a888dd6e1358b4256bb0802a66eb0df0ba8aeee8113c24c",
"role": "User",
"createdAt": {
"$date": "2023-08-09T09:53:56.395Z"
},
"updatedAt": {
"$date": "2023-08-09T09:53:56.395Z"
},
"__v": 0
},
{
"_id": {
"$oid": "64d3627ef42bebe5d16260bf"
},
"favorites": [],
"following": [],
"username": "manuel_rodrigues",
"email": "changeplus64@gmail.com",
"firstname": "Manuel",
"lastname": "Rodrigues",
"phone": "0621745693",
"society": "Change Plus",
"pseudoAC": "",
"salt": "07841463ed0da7f3e47f3a6faf414e93",
"hash": "90b40e506332109636b0285eeb50f9e76b0ac37b9a9fe13b59fb3426754363e7f4c217fff07567f4a7c4ec14a2ad4e5e144863ca52fb1ba2d865d2a62335404b3c79fc19891fe9cbd858eb1d84d178a901ab0b03362f2685775fe5b18d8f51818a337f53b5700bf79b2f639d2c60309af164028fb84e86ca2559a531e62bbfb0aa31246d715f1ab50cffbd1c3687cff8f4d07db1b0345296ba78270216da444dc501ac452842aee292466843290a7d4c5393c633f922c98d075e80dfa888af75cc6db0c15efed3a931a9ea5f27c179c4f5983ef542063df7ea059bdda25d00069652c82d41fd8458d84bac59077d39494d2cf55b87147b0278705b5b871f4509dcfaa720a99a6bc54dd7cf754e9f080c2f2f7a1152904e4164c30487af18c449d5ff7df19c8ce66fba7372bc9c4c2dbe3f45027f6d97642deb7493c43e715c3b143b850c8636b2d2a5975999c6d2d0e4b85156e0f9788a67e391bb05ab796cee2f99e25c908269cc82ea95764437b92b596d22081403363103e305b8352748b77aea2690865dcd782921a6f198908ff8342076475565d50182a9cf0b6fe0d266d829ac9fa67b6f9fa33737aaf68c07c66094fb5efd813fe8ce2e711d17fd95cf1f82096d7314a9b3b19276e22f2f4367e1f3d1d86cde26fd245032ca1cdd02a39712aba6793167853ed3847a6461d56f2d41db4ef9f4b71ac0b78b0d4bd7583e",
"role": "User",
"createdAt": {
"$date": "2023-08-09T09:55:10.737Z"
},
"updatedAt": {
"$date": "2023-08-09T09:55:10.737Z"
},
"__v": 0
},
{
"_id": {
"$oid": "64d362a3f42bebe5d16260d1"
},
"favorites": [],
"following": [],
"username": "frederic_cabon",
"email": "fcabon.numis@yahoo.fr",
"firstname": "Frederic",
"lastname": "Cabon",
"phone": "0556812159",
"society": "Cabon Numismatique",
"pseudoAC": "",
"salt": "ac563ffe5abef7f6d4930d90b7c535d8",
"hash": "1e665a577c9806386c66a6fcfbad5ee0340ca0ba2f3f0379f20d5ff171d3218dad85a6ed885148a2a96e45b3c5758217e49f0ae17260310bc1b21f09ff44bfe5434cd05b90aa4883ab31966a49de9231b46139bd337897610653ffdd39ffedf3bfcb5a31ce34e6ade723e847333bdd0369becf613593d88c4c48ba06aa6b35aabf1940846d5d1452f87ec603b625edd5566987564364e436c4f9cf65b0d849bb8b792216bad471c3e60382c893d37bb5f7c7f8ba0dd515e5062a4d92e85b4c2472a2cc25ee48d6b4e6d8370ad111933852732019c7a33e90fe0114b8b67709581920a5a9054806ec50b5fed7c42458bc3bb3f7c319bee23e7b8ffcd323ad16fe7beadc0a1d00d8ffd7575e4e2cc93410dd46679c7360627ddbc44c8fc0b62cd447dd3092482a43981160219d596911f4a7e99a4975904bb21d719524af2b36355fb7411f01e6e826dd6e98a37d83c1c29216aea24a15a29212f7265b379c4a1aea9afb74c43cf24ee11be2514af6305d7691e92911cd1c56d2e71af29be39bbb3e0653d7bc9d8f1d2f2ac099ead62962bb76f59308ead5ce861d61af8eadc7778954aa25b0ab321952f8cd38cff92ed725aa5cbed1b0a9f9ab35cd2d6194523ed2c6366dbe10ab47898d351fd0ea4bc487cf9f915672c093e7df5416d279af18cd94e5891e520fe092d2af19c223ede6fce3f7954547e2cbf6877a66876e2046",
"role": "User",
"createdAt": {
"$date": "2023-08-09T09:55:47.818Z"
},
"updatedAt": {
"$date": "2023-08-09T09:55:47.818Z"
},
"__v": 0
},
{
"_id": {
"$oid": "64d362d4f42bebe5d16260e1"
},
"favorites": [],
"following": [],
"username": "sebastien_gheneyn",
"email": "sebastien.gheneyn@gmail.com",
"firstname": "Sebastien",
"lastname": "Gheneyn",
"phone": "0630765688",
"society": "Euro GS Finance",
"pseudoAC": "EREGAP",
"salt": "82dc595495fc4f5ced00ffe5f62f0366",
"hash": "311d477ef906c62dcb5a066ac2ef1c9e6521c0a16042a53cd83f73ec79d5a88917c71650bcfbd29d04c06d0cdb87c6cf0b944111b325584b050127000e14f13efea3dc9f105c22d7bf8596c211ba45c1406191dbe1ee8dac9fc9b60bcd0e794165179df46a6482e860bc6b6df2ea8eb3d87be173ec29478576d15e5faebacef158af1c6630df0a247c021bc3a720e7f9a11a368b56b7c69a8505ac70dbd38ab319a7ef314f231768612e421ff1f1f63797fd0704018086e05ecbf4f53db78433b7972d81cf5794d656475331b765f58698f019e523345d8f49d84abb781f0d831161438b946ccda0eecf9eb0b1da20dfa4c22c099ca14384a9ea48af8f01f26239d96713fcca5d48731de9931236708f4a101cd00d257b6e8828759c56c2218fa3f84641cbc62b30e8e05be416f950ce47627734e4d175735275b5c4a14a3724b4500c335e3c6704ddcdd8f8f15f5737c7a69fb3f6854b7f7256f193b98daf7c3fd367172d61b4294ce44962e8ffd7bf3ec362c4854d83ae6b075d27c3039c8bafb2d790410603642dd4f10c9b6a858fd46107dfc37d214e9461e239c27ff9791d9bc5b5a3d0ae6e65eee7aa33413f401deac1c10ae08f087d5c57105f45f3bcd2ace7727dd3819b384606453ff4c142e108bd7de4592dfb5bb283854d9871d6632d872cbd37881001e9e7003a050967d9dbe47a6c48fc77ded9a22493f89b7c",
"role": "User",
"createdAt": {
"$date": "2023-08-09T09:56:36.883Z"
},
"updatedAt": {
"$date": "2023-08-09T09:56:36.883Z"
},
"__v": 0
},
{
"_id": {
"$oid": "64d36325f42bebe5d16260f3"
},
"favorites": [],
"following": [],
"username": "patrice_tassistro",
"email": "sarlctmp@gmail.com",
"firstname": "Patrice",
"lastname": "Tassistro",
"phone": "0688017403",
"society": "CTMP (Comptoir Toulousain Métaux Précieux)",
"pseudoAC": "",
"salt": "f8946d23b20184c05ff9af9fd30f8ba0",
"hash": "65b35ad6ca6240becb63826afe3942e062ab62ca7e2bea1b3a14ee67d558367bfe57d7461c88b1cf44fbeca65418500e3ab1bf4a95f2fe64549616108c2d7558c7e778ff486ec0c315a9cf22f21aefc2df706f1253b76fac39b54fc8cc2888c6ae447a20916148c6b75b6a0595b5cbe462357a64ec368864641615d3343df57440295704eaeb94fed45ad805b6124777700da5589c704105bafb7a0e7c12f5f84e30b12e467362cc94ea4f3e53c93319b5078c7fbdab1ac14f0ff2ce96aa54df444bda9505441eb2d9522913f5c7b41a287ca2ecb0db9ee5be1f92936bbd12b8d26ac2834971e1a2bcf0637fae03d39d6f59eea312291a8930e999ac337f8519589b1cc2cdf6e09c6fef736a6da58f0da4d3cb6553baaa37087a4f2f14e2cb235cb47b1dd3012ee073d3aa0cf21354c2bb8ae5fd2e152743af8ebb5bb9be7d6c7c72f8e4f5ab8bb24d6dbb97d2b77bd5a06d347dd4e1227599c483a288f390aa1ac2c70295bdfd1b70d3e03179750209987d5355aa1f04a53d1cc0d465a973859c7cd2101a85ee488d27636600c5ec225546145f22a9664f6db4a0c3ac7ab3faa84bcf428f882ee632270c1549cf8f103aafcc520af6e1d229f081453ec307535ded8f778088ed0af7bbcc370c17451443903c6705f488c79ce5ba155af5ef5ad8f021396c1e76f667045d8e264a880edb02e4324ddfd3ac1f8ee518a169a96d",
"role": "User",
"createdAt": {
"$date": "2023-08-09T09:57:57.795Z"
},
"updatedAt": {
"$date": "2023-08-09T09:57:57.795Z"
},
"__v": 0
},
{
"_id": {
"$oid": "64d3636bf42bebe5d1626105"
},
"favorites": [],
"following": [],
"username": "franck_gicquel",
"email": "bordeaux@fidso.com",
"firstname": "Franck",
"lastname": "Gicquel",
"phone": "0557999999",
"society": "Or Expertise Fidso",
"pseudoAC": "",
"salt": "d24d34f28257b974145cafe8d38e82c3",
"hash": "6ebf5514b234a418500a03d916bb477b6c2859240ea2918ab38e8cd51c28cc9fb09932c00dad7bca97a5f65e2088d8da42b8863f0fcbfff950c799442f2d2fc8f69a282bcf9332926a6f6a0dbbcb24789793d64730c4f67abef4371af5b849afe8cfb9ba3c9d763557beb2fa13690d961c4310a0e8bc389b8344569412d4425d3eb3d6d1169a9d5b17298b3cb9c867acf966f577661fca549d2567a724d8b9ac2028b4c93c53e41ee3968758a9fb7e40c86953614419566f8ef5e0e9c5dbdc052d6e0c36e512b5a3de7df07134d2c98d01131d061aa2a6c4d497a06daf55ef2757908b29e32a284f70418fcfea1ad6cfc189c2c7ee22cf961b27593c46f2625d1493f339f2093a3aa713904847308fa53aa751ba5134f00555cc6018491db2bf35a25e96a0407c830c3dffbf85e2eda4d3fcfd47b4dc74995baad720d088c0eeee07890dc1907b145ebefa382811d92c1b03af4f1c2f9b246cbce70bad94ead2fcaaaa8b337a3839c51b9c43849e2a2278dfdc2e62319a10b2bbbf49615e66ff953c86e3a8ddb4bfd8bf1e2160a9d7da34b18bd67e6bd9be034d770a96855a026dc5badd2f16bf3175e45e196962a196101a013eb3ab8b8eb1c42b40194a93f9b1a403e5d468d2ebec6f534e9defadf815131a410baf8fd72b9b53b660faa6c70551450515ca28fa9800f66bf9076b6313551e8c3cd085a148672a5a596bb902",
"role": "User",
"createdAt": {
"$date": "2023-08-09T09:59:07.208Z"
},
"updatedAt": {
"$date": "2023-08-09T09:59:07.208Z"
},
"__v": 0
},
{
"_id": {
"$oid": "64d3641ff42bebe5d1626163"
},
"favorites": [],
"following": [],
"username": "julien_dapsens_turquat",
"email": "mail@galerie-des-monnaies.fr",
"firstname": "Julien",
"lastname": "Dapsens-Turquat",
"phone": "0662012604",
"society": "Galerie des Monnaies",
"pseudoAC": "",
"salt": "fc59f59219a8bdfdf87aa951b3154db5",
"hash": "dc8a69c86415d84ec4dd16d95f04971f0deefd55ee83c32b362878cc959f25acc99d528b84f1a889c54b775f6be332010a8fa2f4ee95add677453c0654be7704e6ee0912bbf830655d95080260063f52672854f08b826ccfe6fe643458d9d7f4c79a849ce2045b1b6923ef725c0e42ca326631ae2ed8af43829331909da2a79e610d7397333954cd58c2bb76b0b1dae011af4c038442858134c0c7719c209dcb316ff1a477f52287a1821aea6c99163f4a118f36e2dd3a69f35fae8669dbf44ddc9229567b0e65de070138bc1a2191c2b4eae09ed0c89374549ef1999b8b336743921c517359f43e318a123579a30e13adc1b2669ee0c136c3590b60da555e6baf5d85913590d348c50013bd54f5d0a8d01f3b1dec975982089ed27645ac79b10dc2559efbe893226453b7e2f9d8f91e5d2bcd9f6d98bc67df021fb51454ea8c4dc240310f3655f150d30f0b421aee1723ca9d42d1e949577410111ed81fb36f5256f191e16090e0703754d01b5f22be7a78e1ef8982d04bf8c539714e1c8d084ae65cd0f11738a28da21ffed2f44000c4df19b84dd7a342fd2658ba3578085187a459800caa0761d65b2dcf24dcf4bdf13afefcbb1d2eb8c56047017d51eb2dcc9c7f02d1dbed594386ae188cf7658b3e4bb5dd65299d5ab6370285e0a21f1be43edd29f3803e8e7b8dbac04626a90fa94ac23831885ae7fa09db1b3028757e",
"role": "User",
"createdAt": {
"$date": "2023-08-09T10:02:08.050Z"
},
"updatedAt": {
"$date": "2023-08-09T10:02:38.743Z"
},
"__v": 0,
"image": "/assets/images/user_avatar.jpg"
},
{
"_id": {
"$oid": "64d397d9f42bebe5d162619e"
},
"favorites": [],
"following": [],
"username": "antoine_vincent",
"email": "avincent@aucoffre.com",
"firstname": "Antoine",
"lastname": "Vincent",
"phone": "0788834747",
"society": "AuCoffre",
"pseudoAC": "",
"salt": "fb310aa95696dcec3351d03c9a68855b",
"hash": "2f00be603829a8c30f89b9d6a9bc0a0123aa921c31d10246e3000ca93ae329a0b12e54eec5091a5c30fec9e3c9bcfc9cdad2991786ed2f87a19618e1ac2521754d0329235709b86de82f655903f75d680fb0bc5836017f0a1d54c28212f4eb81f9255d3bc24815ba67a922498b1ef0e0c4ae125f5346d6abf0136663ff8df1c9eac8974fe9a9874bf699877652375c12fa2618ce96fed6a1eedf205a2311ada78d829d48fa7f688a6e820fe1dfa2762a430bf7132ef6826005fa186ce849fc6dbda112983d8504e53d5cf5a3bcd160e62905a72ece2c7e73d0b0c3163ecc09c217084d9009b0f301b9235961b7286a06ee235ab980d1eee37128bfd5b9b65d46377ca315eaeb283192d80e0d83fda5eac79cd9c39b9cdaf0cb11b553204a8cd27fb678550fb3d6676f94f5e8e1727d0d94914993d75c8142d6d8792da497ebb76a93d006850fd17bad826cfae6367dd315740293353b22f23553a43e4689678bcfe553500e6a49605a6f7668c6d03c05705a3365986119a57cfdcb9c4b6a58e73aa9cbbed1719673dc08b3a5da06e28cad2775fce88a5d844c9deb29bb23b50e36ae9fb813d95b6919eea975f38d47401cca8ff2e7320b5ead2b2a3481167129b97c4432fc9f4f0d658c5cc6be300a90fdcf4864291bafd09d209fbeac74ef80606f3c9a1b4249413e277067b31d9100cf7974c6c017f5597c0b442dc4e2dfed",
"role": "User",
"createdAt": {
"$date": "2023-08-09T13:42:49.353Z"
},
"updatedAt": {
"$date": "2023-08-09T13:42:49.353Z"
},
"__v": 0
}]
@@ -0,0 +1,112 @@
[{
"_id": {
"$oid": "652075758a2b41cd02ad2f4c"
},
"num": 1,
"name": "Aéronefs",
"questions": []
},
{
"_id": {
"$oid": "652075758a2b41cd02ad2f4d"
},
"num": 2,
"name": "Altimètre",
"questions": []
},
{
"_id": {
"$oid": "652075758a2b41cd02ad2f4e"
},
"num": 3,
"name": "Déclencheurs et matériels",
"questions": []
},
{
"_id": {
"$oid": "652075758a2b41cd02ad2f4f"
},
"num": 4,
"name": "Largage",
"questions": []
},
{
"_id": {
"$oid": "652075758a2b41cd02ad2f50"
},
"num": 5,
"name": "Règlementation",
"questions": []
},
{
"_id": {
"$oid": "652075758a2b41cd02ad2f51"
},
"num": 6,
"name": "Pilotage et mécanique de vol",
"questions": []
},
{
"_id": {
"$oid": "652075758a2b41cd02ad2f52"
},
"num": 7,
"name": "Physiologie",
"questions": []
},
{
"_id": {
"$oid": "652075758a2b41cd02ad2f53"
},
"num": 8,
"name": "Connaissances générales",
"questions": []
},
{
"_id": {
"$oid": "652075758a2b41cd02ad2f54"
},
"num": 9,
"name": "Unités de mesure",
"questions": []
},
{
"_id": {
"$oid": "652075758a2b41cd02ad2f55"
},
"num": 10,
"name": "Altimétrie",
"questions": []
},
{
"_id": {
"$oid": "652075758a2b41cd02ad2f56"
},
"num": 11,
"name": "Météorologie et aérologie",
"questions": []
},
{
"_id": {
"$oid": "652075758a2b41cd02ad2f57"
},
"num": 12,
"name": "Sauts spéciaux",
"questions": []
},
{
"_id": {
"$oid": "652075758a2b41cd02ad2f58"
},
"num": 13,
"name": "Questions spécifiques au brevet C",
"questions": []
},
{
"_id": {
"$oid": "652075758a2b41cd02ad2f59"
},
"num": 14,
"name": "Questions spécifiques au brevet D",
"questions": []
}]
+143
View File
@@ -0,0 +1,143 @@
[
{
"_id": {
"$oid": "651f4b5beb17b73b9333b777"
},
"name": "bpa",
"categories": [
{
"$oid": "652075758a2b41cd02ad2f4c"
},
{
"$oid": "652075758a2b41cd02ad2f4d"
},
{
"$oid": "652075758a2b41cd02ad2f4e"
},
{
"$oid": "652075758a2b41cd02ad2f4f"
},
{
"$oid": "652075758a2b41cd02ad2f50"
},
{
"$oid": "652075758a2b41cd02ad2f51"
},
{
"$oid": "652075758a2b41cd02ad2f52"
},
{
"$oid": "652075758a2b41cd02ad2f53"
},
{
"$oid": "652075758a2b41cd02ad2f54"
},
{
"$oid": "652075758a2b41cd02ad2f55"
},
{
"$oid": "652075758a2b41cd02ad2f56"
},
{
"$oid": "652075758a2b41cd02ad2f57"
}
]
},
{
"_id": {
"$oid": "651f4b91eb17b73b9333b778"
},
"name": "c",
"categories": [
{
"$oid": "652075758a2b41cd02ad2f4c"
},
{
"$oid": "652075758a2b41cd02ad2f4d"
},
{
"$oid": "652075758a2b41cd02ad2f4e"
},
{
"$oid": "652075758a2b41cd02ad2f4f"
},
{
"$oid": "652075758a2b41cd02ad2f50"
},
{
"$oid": "652075758a2b41cd02ad2f51"
},
{
"$oid": "652075758a2b41cd02ad2f52"
},
{
"$oid": "652075758a2b41cd02ad2f53"
},
{
"$oid": "652075758a2b41cd02ad2f54"
},
{
"$oid": "652075758a2b41cd02ad2f55"
},
{
"$oid": "652075758a2b41cd02ad2f56"
},
{
"$oid": "652075758a2b41cd02ad2f57"
},
{
"$oid": "652075758a2b41cd02ad2f58"
}
]
},
{
"_id": {
"$oid": "651f4bb2eb17b73b9333b779"
},
"name": "d",
"categories": [
{
"$oid": "652075758a2b41cd02ad2f4c"
},
{
"$oid": "652075758a2b41cd02ad2f4d"
},
{
"$oid": "652075758a2b41cd02ad2f4e"
},
{
"$oid": "652075758a2b41cd02ad2f4f"
},
{
"$oid": "652075758a2b41cd02ad2f50"
},
{
"$oid": "652075758a2b41cd02ad2f51"
},
{
"$oid": "652075758a2b41cd02ad2f52"
},
{
"$oid": "652075758a2b41cd02ad2f53"
},
{
"$oid": "652075758a2b41cd02ad2f54"
},
{
"$oid": "652075758a2b41cd02ad2f55"
},
{
"$oid": "652075758a2b41cd02ad2f56"
},
{
"$oid": "652075758a2b41cd02ad2f57"
},
{
"$oid": "652075758a2b41cd02ad2f58"
},
{
"$oid": "652075758a2b41cd02ad2f59"
}
]
}
]
+81
View File
@@ -0,0 +1,81 @@
<div class="d-flex">
<div class="flex-fill">
<h1 class="mb-1">
{{ title }}
<span class="ms-2 text-muted fs-4">{{ questionsCount }} {{ questionsCount > 1 ? 'questions' : 'question' }}</span>
</h1>
<span class="me-2 fs-6">{{ subtitle }}</span>
</div>
<span class="flex-spacer"></span>
<div class="align-self-center me-2">
<button mat-icon-button [matMenuTriggerFor]="menuAeronef" aria-label="Menu Aeronef">
<mat-icon fontIcon="more_vert"></mat-icon>
</button>
<mat-menu #menuAeronef="matMenu">
@for (menuitem of menuItems.getMenuPanel(); track menuitem) {
<button mat-menu-item>
<mat-icon [fontIcon]="menuitem.icon"></mat-icon>
<span>{{ menuitem.name }}</span>
</button>
}
</mat-menu>
</div>
</div>
<mat-divider class="my-3"></mat-divider>
<app-list-errors [errors]="errors"></app-list-errors>
@if (questions.controls.length) {
<mat-card [hidden]="!questions.controls.length">
<div class="mx-3 my-5">
<mat-progress-bar [mode]="loading ? 'query' : 'determinate'" [value]="loading ? 0 : (totalAnswer/questionsCount*100)" color="accent"></mat-progress-bar>
<mat-progress-bar class="mt-0" [mode]="loading ? 'query' : 'determinate'" [value]="loading ? 0 : ((timeProgress/timeMax)*100)" [color]="timeProgressColor"></mat-progress-bar>
<ol class="squareItems clearfix">
@for (answer of answers; track $index) {
<li [class]="answer">{{ ($index + 1) }}</li>
}
</ol>
</div>
<mat-card-content>
<form [formGroup]="qcmForm" (ngSubmit)="onSubmit()">
@for (category of qcm.categories; track category; let index = $index) {
<mat-accordion>
<mat-expansion-panel class="mb-3" [expanded]="expandedIndex === index" (opened)="setExpandedIndex(index)">
<mat-expansion-panel-header>
<mat-panel-title class="fs-4 d-flex align-items-center justify-content-between">
<span><span class="text-navy-light">{{ category.num }} - </span>{{ category.name }}</span>
</mat-panel-title>
<mat-panel-description>
{{ category.questions.length }} {{ category.questions.length > 1 ? 'questions' : 'question' }}
- 0 réponse
</mat-panel-description>
</mat-expansion-panel-header>
<div class="mb-3 ms-4" formArrayName="questions">
@for (question of category.questions; track question; let index = $index) {
<div class="mb-3" [formGroupName]="index">
<label class="fs-5 fw-medium mb-2" for="radio-group-{{ category.num }}-{{ question.num }}">{{ question.num }} - {{ question.libelle }}</label>
<mat-radio-group formControlName="choice" id="radio-group-{{ category.num }}-{{ question.num }}" class="d-flex flex-column" (change)="onChange(category, question, $event.value)">
@for (choice of question.choices; track choice) {
<mat-radio-button [value]="choice.index" [color]="choice.correct ? 'primary' : 'warn'">{{ choice.libelle }}</mat-radio-button>
}
</mat-radio-group>
</div>
}
</div>
</mat-expansion-panel>
</mat-accordion>
}
</form>
</mat-card-content>
<!--
<form [formGroup]="qcmForm">
<div class="mb-3 ms-4" formArrayName="questions">
@for (question of questions.controls; track question; let index = $index) {
<div class="mb-3" [formGroupName]="index">
<label class="fs-5 fw-medium mb-2" for="radio-group-{{ index }}">{{ index }} - Libellé</label>
<input formControlName="choice" type="text"/>
</div>
}
</div>
</form>
-->
</mat-card>
}
+37
View File
@@ -0,0 +1,37 @@
/* QCM */
@use 'variable' as var;
ol.squareItems {
padding: 0;
margin: 1rem 0;
li {
color: var.$primary;
background-color: var.$sidenav-content; //var.$dark;
background-image: none;
border: 1px solid var.$dark; //var.$extra-muted;
cursor: pointer;
float: left;
list-style-type: none;
margin: 0.17em;
padding: 0.34em 0;
text-align: center;
width: 33px;
height: 33px;
&.active {
border-color: var.$info;
}
&.valid {
color: var.$sidenav-content;
border-color: var.$success;
background-color: var.$primary;
}
&.defer {
border-color: var.$info-dark;
background-color: var.$navy-light;
}
&.error {
border-color: var.$purple-light;
background-color: var.$secondary;
}
}
}
@@ -0,0 +1,21 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { QcmComponent } from './qcm.component';
describe('QcmComponent', () => {
let component: QcmComponent;
let fixture: ComponentFixture<QcmComponent>;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [QcmComponent]
});
fixture = TestBed.createComponent(QcmComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
+218
View File
@@ -0,0 +1,218 @@
import { Component, DestroyRef, inject, OnInit, OnDestroy } from '@angular/core';
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { /*FormControl,*/ FormGroup, FormBuilder, FormArray, FormsModule, ReactiveFormsModule } from '@angular/forms';
import { ActivatedRoute } from '@angular/router';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatDividerModule } from '@angular/material/divider';
import { MatExpansionModule } from '@angular/material/expansion';
import { MatIconModule } from '@angular/material/icon';
import { MatMenuModule } from '@angular/material/menu';
import { MatProgressBarModule } from '@angular/material/progress-bar';
import { MatRadioModule } from '@angular/material/radio';
import { MatSelectModule } from '@angular/material/select';
import { interval, Observable, Subscription } from 'rxjs';
import { take } from 'rxjs/operators';
import { MenuItems } from 'src/app/components/shared';
import { ListErrorsComponent } from 'src/app/components/shared';
import { Errors, Qcm, QcmCategory, QcmQuestion, QcmQuestionState, User } from 'src/app/core/models';
import { QcmService, UserService } from 'src/app/core/services';
import data from 'src/qcm-bpa.json';
@Component({
selector: 'app-qcm',
standalone: true,
imports: [
FormsModule, ReactiveFormsModule,
MatButtonModule, MatCardModule, MatDividerModule, MatExpansionModule,
MatIconModule, MatMenuModule, MatProgressBarModule, MatRadioModule,
MatSelectModule,
ListErrorsComponent
],
templateUrl: './qcm.component.html',
styleUrl: './qcm.component.scss'
})
export class QcmComponent implements OnInit, OnDestroy {
private _currentUser: Subscription = new Subscription();
private _data: Subscription = new Subscription();
private _timer: Subscription = new Subscription();
private _refresh = 1000;
public qcm: Qcm = {} as Qcm;
public currentUser: User = {} as User;
public canModify = false;
public title = 'QCM Brevets';
public subtitle = 'Préparation au questionnaires';
public errors: Errors = { errors: {} };
public destroyRef = inject(DestroyRef);
public expandedIndex = 0;
public questionsCount = 0;
public answers: Array<QcmQuestionState> = [];
public qcmForm!: FormGroup;
public loading = true;
public totalAnswer = 0;
public timeProgress = 0;
public timeProgressColor = 'primary';
public timeMax = (60*45);
constructor(
private route: ActivatedRoute,
private fb: FormBuilder,
private _qcmService: QcmService,
private _userService: UserService,
public menuItems: MenuItems
) {
this.qcmForm = this.fb.group({
questions: this.fb.array([])
});
}
get questions(): FormArray {
return this.qcmForm.controls['questions'] as FormArray;
}
ngOnInit() {
//this._importQuestions();
//this._importChoices();
// Retreive the prefetched QCM
const data$: Observable<{ qcm: Qcm }> = this.route.data as Observable<{ qcm: Qcm }>;
this._data = data$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((data: { qcm: Qcm }) => {
this.questionsCount = 0;
this.qcm = data.qcm;
if (this.qcm.name === 'bpa') {
this.title = 'QCM du BPA';
this.subtitle = 'Préparation au questionnaire du BPA';
} else {
this.title = `QCM du brevet ${this.qcm.name.toUpperCase()}`;
this.subtitle = `Préparation au questionnaire du brevet ${this.qcm.name.toUpperCase()}`;
}
//console.log(this.title, this.qcm);
this.qcm.categories.forEach(category => {
this.questionsCount += category.questions.length;
category.questions.forEach(question => {
this.addQuestion(question);
});
});
});
// Load the current user's data
const currentUser$: Observable<User> = this._userService.currentUser;
this._currentUser = currentUser$.subscribe((userData: User) => {
this.currentUser = userData;
this.canModify = userData.role === 'Admin';
});
this.loading = false;
}
ngOnDestroy() {
this._data.unsubscribe();
this._currentUser.unsubscribe();
}
private _starTimer(): void {
this._timer = interval(1000)
.pipe(take(this.timeMax))
.subscribe(() => {
this.timeProgress++;
if (this.timeProgress > (this.timeMax/100*70) && this.timeProgress <= (this.timeMax/100*90)) {
this.timeProgressColor = 'warning';
} else if (this.timeProgress > (this.timeMax/100*90) && this.timeProgress <= (this.timeMax/100*98)) {
this.timeProgressColor = 'danger';
} else if (this.timeProgress > (this.timeMax/100*98)) {
this.timeProgressColor = 'purple';
}
if (this.timeProgress >= this.timeMax) {
this.timeProgressColor = 'accent';
this._timer.unsubscribe();
}
});
}
addQuestion(question: QcmQuestion) {
const item = this.fb.group({
id: [question.num],
choice: ['']
});
this.answers.push(QcmQuestionState.UNANSWERED);
this.questions.push(item);
}
setExpandedIndex(index: number) {
this.expandedIndex = index;
}
onChange(category: QcmCategory, question: QcmQuestion, value: unknown) {
//console.log('onChange', category.num, question.num, value);
if (this.timeProgress === 0) {
this._starTimer();
}
this.qcm.categories[(category.num-1)].questions[(question.num-1)].choices.forEach(choice => {
if (choice.index === value) {
if (choice.correct) {
this.answers[(question.num-1)] = QcmQuestionState.VALID;
console.log(`Réponse à la question ${question.num} de la catégorie ${category.num} : correcte`);
} else {
this.answers[(question.num-1)] = QcmQuestionState.ERROR;
console.log(`Réponse à la question ${question.num} de la catégorie ${category.num} : incorrecte`);
}
}
});
this.totalAnswer++;
this.questions.controls[(question.num-1)].disable();
//console.log(this.questions.controls[(question.num-1)].disabled);
}
onSubmit() {
console.log('onSubmit');
}
private _importChoices() {
try {
data.categories.forEach((category) => {
category.questions.forEach(question => {
this._qcmService.saveChoices(question)
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe({
next: (question) => {
console.log('question : ', question);
//console.log(`Le saut n°${jump.numero} a été ajouté !`, 'OK');
},
error: (err) => {
console.log('err : ', err);
//console.log(`Le saut n'a pas été ajouté !`, 'Error', err);
this.errors = err;
}
});
});
});
} catch (error) {
console.error(error);
}
}
private _importQuestions() {
try {
data.categories.forEach((category) => {
//Object.assign(this.jump, category);
this._qcmService.saveQuestions(category)
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe({
next: (category) => {
console.log('category : ', category);
//console.log(`Le saut n°${jump.numero} a été ajouté !`, 'OK');
},
error: (err) => {
console.log('err : ', err);
//console.log(`Le saut n'a pas été ajouté !`, 'Error', err);
this.errors = err;
}
});
});
} catch (error) {
console.error(error);
}
}
private _resetErrors(): void {
this.errors = { errors: {} };
}
}

Some files were not shown because too many files have changed in this diff Show More