---|qcm| Mise à jour V17 et QCM

This commit is contained in:
Rampeur
2024-05-04 02:08:04 +02:00
parent fa93c0c8f2
commit 6dece821a8
241 changed files with 27526 additions and 6520 deletions
+5
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
@@ -45,6 +46,10 @@ testem.log
.DS_Store .DS_Store
Thumbs.db Thumbs.db
# SonarQube
.scannerwork/
sonar.properties
**/*.copy.ts **/*.copy.ts
**/*.copy.scss **/*.copy.scss
**/*.copy.html **/*.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
+169 -152
View File
@@ -1,155 +1,172 @@
{ {
"$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 "analytics": false,
}, "schematicCollections": [
"newProjectRoot": "projects", "@angular-eslint/schematics"
"projects": { ]
"headup_app": { },
"projectType": "application", "newProjectRoot": "projects",
"schematics": { "projects": {
"@schematics/angular:component": { "headup_angular": {
"style": "scss" "projectType": "application",
} "schematics": {
}, "@schematics/angular:component": {
"root": "", "style": "scss"
"sourceRoot": "src",
"prefix": "huapp",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": "dist/headup_app",
"index": "src/index.html",
"main": "src/main.ts",
"tsConfig": "tsconfig.app.json",
"polyfills": [
"zone.js"
],
"inlineStyleLanguage": "scss",
"assets": [
"src/favicon.ico",
"src/assets",
"src/manifest.webmanifest"
],
"styles": [
"node_modules/chartist/dist/index.scss",
"src/styles/styles.scss"
],
"scripts": [],
"vendorChunk": true,
"extractLicenses": false,
"buildOptimizer": false,
"sourceMap": true,
"optimization": false,
"namedChunks": true
},
"configurations": {
"production": {
"optimization": true,
"outputHashing": "all",
"sourceMap": false,
"namedChunks": false,
"extractLicenses": true,
"vendorChunk": false,
"buildOptimizer": true,
"budgets": [
{
"type": "initial",
"maximumWarning": "500kb",
"maximumError": "1mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "2kb",
"maximumError": "4kb"
}
]
},
"development": {
"optimization": false,
"sourceMap": true,
"namedChunks": true,
"extractLicenses": false,
"vendorChunk": true,
"buildOptimizer": false,
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.development.ts"
}
],
"budgets": [
{
"type": "anyComponentStyle",
"maximumWarning": "6kb"
}
]
},
"local": {
"optimization": false,
"sourceMap": true,
"namedChunks": true,
"extractLicenses": false,
"vendorChunk": true,
"buildOptimizer": false,
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.local.ts"
}
],
"budgets": [
{
"type": "anyComponentStyle",
"maximumWarning": "6kb"
}
]
}
},
"defaultConfiguration": "production"
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"configurations": {
"production": {
"buildTarget": "headup_app:build:production"
},
"development": {
"buildTarget": "headup_app:build:development"
}
},
"defaultConfiguration": "development"
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"buildTarget": "headup_app:build"
}
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"polyfills": [
"zone.js",
"zone.js/testing"
],
"tsConfig": "tsconfig.spec.json",
"inlineStyleLanguage": "scss",
"assets": [
"src/favicon.ico",
"src/assets",
"src/manifest.webmanifest"
],
"styles": [
"src/styles/styles.scss"
],
"scripts": []
}
}
}
} }
},
"i18n": {
"sourceLocale": "fr"
},
"root": "",
"sourceRoot": "src",
"prefix": "app",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser-esbuild",
"options": {
"outputPath": "dist/headup_angular",
"index": "src/index.html",
"main": "src/main.ts",
"tsConfig": "tsconfig.app.json",
"polyfills": [
"zone.js"
],
"inlineStyleLanguage": "scss",
"assets": [
"src/favicon.ico",
"src/assets",
"src/manifest.webmanifest",
{
"glob": "_redirects",
"input": "src",
"output": "/"
}
],
"styles": [
"node_modules/chartist/dist/index.scss",
"src/styles/styles.scss"
],
"stylePreprocessorOptions": {
"includePaths": ["src/styles"]
},
"scripts": []
},
"configurations": {
"production": {
"buildOptimizer": true,
"extractLicenses": true,
"namedChunks": false,
"optimization": true,
"outputHashing": "all",
"sourceMap": false,
"budgets": [
{
"type": "initial",
"maximumWarning": "500kb",
"maximumError": "1mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "2kb",
"maximumError": "4kb"
}
]
},
"development": {
"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.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"
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"configurations": {
"production": {
"buildTarget": "headup_angular:build:production"
},
"development": {
"buildTarget": "headup_angular:build:development"
},
"local": {
"buildTarget": "headup_angular:build:local"
}
},
"defaultConfiguration": "development"
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"buildTarget": "headup_angular:build"
}
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"polyfills": [
"zone.js",
"zone.js/testing"
],
"tsConfig": "tsconfig.spec.json",
"inlineStyleLanguage": "scss",
"assets": [
"src/favicon.ico",
"src/assets",
"src/manifest.webmanifest"
],
"styles": [
"src/styles/styles.scss"
],
"scripts": []
}
},
"lint": {
"builder": "@angular-eslint/builder:lint",
"options": {
"lintFilePatterns": [
"src/**/*.ts",
"src/**/*.html"
]
}
}
}
} }
} }
}
+3481 -1378
View File
File diff suppressed because it is too large Load Diff
+34 -26
View File
@@ -1,57 +1,65 @@
{ {
"name": "headup-app", "name": "headup-app",
"version": "1.0.0", "version": "1.0.0",
"author": "Solide Apps <contact@rampeur.fr>", "author": "Solide Apps <admin@unespace.com>",
"contributors": [ "contributors": [
"Julien Gautier <rampeur@gmail.com>" "Julien Gautier <julien.gautier@unespace.com>"
], ],
"scripts": { "scripts": {
"ng": "ng", "ng": "ng",
"start": "ng serve --configuration development", "start": "ng serve",
"build": "ng build", "build": "ng build",
"watch": "ng build --watch --configuration development", "watch": "ng build --watch --configuration development",
"local": "ng serve --configuration local", "dev": "ng serve --configuration local",
"dev": "APP_ENV=local ng serve --configuration development", "local": "APP_ENV=local ng serve --port 4400 --configuration local",
"lint": "ng lint", "lint": "ng lint",
"test": "ng test" "test": "ng test"
}, },
"engines": { "engines": {
"node": "^16.20.2 || ^18.19.1 || ^20.11.1" "node": "^18.19.1 || ^20.11.1"
}, },
"license": "UNLICENSED",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@angular/animations": "^17.2.3", "@angular/animations": "^17.3.5",
"@angular/cdk": "^17.2.1", "@angular/cdk": "^17.3.5",
"@angular/common": "^17.2.3", "@angular/common": "^17.3.5",
"@angular/compiler": "^17.2.3", "@angular/compiler": "^17.3.5",
"@angular/core": "^17.2.3", "@angular/core": "^17.3.5",
"@angular/forms": "^17.2.3", "@angular/forms": "^17.3.5",
"@angular/material": "^17.2.1", "@angular/localize": "^17.3.5",
"@angular/platform-browser": "^17.2.3", "@angular/material": "^17.3.6",
"@angular/platform-browser-dynamic": "^17.2.3", "@angular/platform-browser": "^17.3.5",
"@angular/router": "^17.2.3", "@angular/platform-browser-dynamic": "^17.3.5",
"@angular/router": "^17.3.5",
"bootstrap": "^5.3.3", "bootstrap": "^5.3.3",
"chart.js": "^4.4.1", "chart.js": "^4.4.2",
"chartist": "^1.3.0", "chartist": "^1.3.0",
"ng-chartist": "^8.2.0", "ng-chartist": "^8.2.0",
"ng2-charts": "^5.0.4", "ng2-charts": "^6.0.0",
"ngx-skeleton-loader": "^8.1.0", "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.14.4" "zone.js": "^0.14.4"
}, },
"devDependencies": { "devDependencies": {
"@angular-devkit/build-angular": "^17.2.1", "@angular-devkit/build-angular": "^17.3.5",
"@angular/cli": "~17.2.1", "@angular-eslint/builder": "17.3.0",
"@angular/compiler-cli": "^17.2.3", "@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.5",
"@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.3.3" "typescript": "~5.3.2"
} }
} }
+1
View File
@@ -0,0 +1 @@
/* /index.html 200
-48
View File
@@ -1,48 +0,0 @@
import { NgModule } from '@angular/core';
import { Routes, RouterModule, PreloadAllModules } from '@angular/router';
const routes: Routes = [
{
path: 'calculator',
loadChildren: () => import('./components/calculator/calculator.module').then(m => m.CalculatorModule)
},
{
path: 'dashboard',
loadChildren: () => import('./components/dashboard/dashboard.module').then(m => m.DashboardModule)
},
{
path: 'jump',
loadChildren: () => import('./components/jump/jump.module').then(m => m.JumpModule)
},
{
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: 'qcm',
loadChildren: () => import('./components/qcm/qcm.module').then(m => m.QCMModule)
},
{
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 -12
View File
@@ -1,12 +1 @@
<!-- * * * * * * * * * * * * * * * * * * * * * * * * * * * * * --> <app-full-layout></app-full-layout>
<!-- * * * * * * * * * * Main wrapper * * * * * * * * * * -->
<!-- * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -->
<huapp-full-layout></huapp-full-layout>
<!--
<div class="p-5">
<router-outlet><huapp-spinner></huapp-spinner></router-outlet>
</div>
<huapp-layout-header></huapp-layout-header>
<router-outlet><huapp-spinner></huapp-spinner></router-outlet>
<huapp-layout-footer></huapp-layout-footer>
-->
+21 -21
View File
@@ -1,29 +1,29 @@
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);
const app = fixture.componentInstance; const app = fixture.componentInstance;
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');
}); });
}); });
+29 -6
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) { }
@@ -17,4 +40,4 @@ export class AppComponent {
this.userService.populate(); this.userService.populate();
this.titleService.setTitle(this.title); this.titleService.setTitle(this.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,
}
]
};
-34
View File
@@ -1,34 +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 { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { CoreModule } from './core/core.module';
import { AuthModule } from './components/auth/auth.module';
import { DashboardModule } from './components/dashboard/dashboard.module';
import { SpinnerComponent } from './components/shared';
import { FullComponent } from 'src/app/components/shared/layout';
//import { HeaderComponent, FooterComponent } from 'src/app/components/shared/layout';
registerLocaleData(localeFr);
@NgModule({
declarations: [ AppComponent ],
imports: [
BrowserModule,
CoreModule,
AuthModule,
AppRoutingModule,
BrowserAnimationsModule,
SpinnerComponent,
FullComponent,
DashboardModule
//HeaderComponent, FooterComponent
],
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,51 @@
<div @flyInOut>
<div class="content">
<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>
</div>
@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { AeronefComponent } from './aeronef.component';
describe('AeronefComponent', () => {
let component: AeronefComponent;
let fixture: ComponentFixture<AeronefComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [AeronefComponent]
})
.compileComponents();
fixture = TestBed.createComponent(AeronefComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -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-aeronef',
standalone: true,
imports: [
DatePipe,
MatBadgeModule, MatButtonModule, MatCardModule, MatDividerModule,
MatExpansionModule, MatGridListModule, MatIconModule, MatMenuModule,
BarsChartComponent, HistoryTableComponent, PieChartComponent
],
templateUrl: './aeronef.component.html',
styleUrl: './aeronef.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 AeronefComponent 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 { }
+110 -74
View File
@@ -1,77 +1,113 @@
<div class="container content auth-page"> <div class="container content 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>
</p> }
<huapp-list-errors [errors]="errors"></huapp-list-errors> @if (authType === 'login') {
<form [formGroup]="authForm" (ngSubmit)="submitForm()"> <a [routerLink]="['/register']">Vous n'avez pas encore de compte ?</a>
<fieldset [disabled]="isSubmitting" class="w-100"> }
<legend [hidden]="true">Informations personnelles</legend> </p>
<div class="col-12"> <app-list-errors [errors]="errors"></app-list-errors>
<mat-form-field appearance="fill" class="w-100"> <form [formGroup]="authForm" (ngSubmit)="submitForm()">
<input matInput type="text" placeholder="Votre adresse email" formControlName="email" required> <fieldset [disabled]="isSubmitting" class="w-100">
<mat-error *ngIf="this.authForm.controls['email'].status === 'INVALID'">{{getErrorMessage('email')}}</mat-error> <legend [hidden]="true">Informations personnelles</legend>
</mat-form-field> <div class="col-12">
</div> <mat-form-field appearance="fill" class="w-100">
<div class="col-12" *ngIf="authType === 'register'"> <input matInput type="text" placeholder="Votre adresse email" formControlName="email" required>
<mat-form-field appearance="fill" class="w-100"> @if (this.authForm.controls['email'].status === 'INVALID') {
<input matInput type="text" placeholder="Votre nom" formControlName="lastname" required> <mat-error>{{getErrorMessage('email')}}</mat-error>
<mat-error *ngIf="this.authForm.controls['lastname'].status === 'INVALID'">{{getErrorMessage('lastname')}}</mat-error> }
</mat-form-field> </mat-form-field>
</div> </div>
<div class="col-12" *ngIf="authType === 'register'"> @if (authType === 'register') {
<mat-form-field appearance="fill" class="w-100"> <div class="col-12">
<input matInput type="text" placeholder="Votre prénom" formControlName="firstname" required> <mat-form-field appearance="fill" class="w-100">
<mat-error *ngIf="this.authForm.controls['firstname'].status === 'INVALID'">{{getErrorMessage('firstname')}}</mat-error> <input matInput type="text" placeholder="Votre nom" formControlName="lastname" required>
</mat-form-field> @if (this.authForm.controls['lastname'].status === 'INVALID') {
</div> <mat-error>{{getErrorMessage('lastname')}}</mat-error>
<div class="col-12" *ngIf="authType === 'register'"> }
<mat-form-field appearance="fill" class="w-100"> </mat-form-field>
<input matInput type="text" placeholder="Numéro de téléphone" formControlName="phone" required> </div>
<mat-error *ngIf="this.authForm.controls['phone'].status === 'INVALID'">{{getErrorMessage('phone')}}</mat-error> }
</mat-form-field> @if (authType === 'register') {
</div> <div class="col-12">
<div class="col-12" *ngIf="authType === 'register'"> <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="Pseudo" formControlName="username"> @if (this.authForm.controls['firstname'].status === 'INVALID') {
<mat-error *ngIf="this.authForm.controls['username'].status === 'INVALID'">{{getErrorMessage('username')}}</mat-error> <mat-error>{{getErrorMessage('firstname')}}</mat-error>
</mat-form-field> }
</div> </mat-form-field>
<div class="col-12" *ngIf="authType === 'register'"> </div>
<mat-form-field appearance="fill" class="w-100"> }
<input matInput type="text" placeholder="Numéro de licence FFP" formControlName="licence"> @if (authType === 'register') {
<mat-error *ngIf="this.authForm.controls['licence'].status === 'INVALID'">{{getErrorMessage('licence')}}</mat-error> <div class="col-12">
</mat-form-field> <mat-form-field appearance="fill" class="w-100">
</div> <input matInput type="text" placeholder="Numéro de téléphone" formControlName="phone" required>
<div class="col-12" *ngIf="authType === 'register'"> @if (this.authForm.controls['phone'].status === 'INVALID') {
<mat-form-field appearance="fill" class="w-100"> <mat-error>{{getErrorMessage('phone')}}</mat-error>
<input matInput type="text" placeholder="Indiquez le poids en Kg" formControlName="poids"> }
<mat-error *ngIf="this.authForm.controls['poids'].status === 'INVALID'">{{getErrorMessage('poids')}}</mat-error> </mat-form-field>
</mat-form-field> </div>
</div> }
</fieldset> @if (authType === 'register') {
<fieldset [disabled]="isSubmitting" class="w-100"> <div class="col-12">
<legend [hidden]="true">Informations de connexion</legend> <mat-form-field appearance="fill" class="w-100">
<div class="col-12"> <input matInput type="text" placeholder="Pseudo" formControlName="username">
<mat-form-field appearance="fill" class="w-100"> @if (this.authForm.controls['username'].status === 'INVALID') {
<input matInput type="password" placeholder="Mot de passe" formControlName="password" required> <mat-error>{{getErrorMessage('username')}}</mat-error>
<mat-error *ngIf="this.authForm.controls['password'].status === 'INVALID'">{{getErrorMessage('password')}}</mat-error> }
</mat-form-field> </mat-form-field>
</div> </div>
<div class="col-12" *ngIf="authType === 'register'"> }
<mat-form-field appearance="fill" class="w-100"> @if (authType === 'register') {
<input matInput type="password" placeholder="Confirmez votre nouveau mot de passe" formControlName="confirmPassword" required> <div class="col-12">
<mat-error *ngIf="this.authForm.controls['confirmPassword'].status === 'INVALID'">{{getErrorMessage('confirmPassword')}}</mat-error> <mat-form-field appearance="fill" class="w-100">
</mat-form-field> <input matInput type="text" placeholder="Numéro de licence FFP" formControlName="licence">
</div> @if (this.authForm.controls['licence'].status === 'INVALID') {
<div class="col-12"> <mat-error>{{getErrorMessage('licence')}}</mat-error>
<button mat-raised-button color="primary" [disabled]="!authForm.valid" type="submit" class="float-sm-end">{{ btnTitle }}</button> }
</div> </mat-form-field>
</fieldset> </div>
</form> }
</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 [disabled]="isSubmitting" class="w-100">
<legend [hidden]="true">Informations de connexion</legend>
<div class="col-12">
<mat-form-field appearance="fill" class="w-100">
<input matInput type="password" placeholder="Mot de passe" formControlName="password" required>
@if (this.authForm.controls['password'].status === 'INVALID') {
<mat-error>{{getErrorMessage('password')}}</mat-error>
}
</mat-form-field>
</div>
@if (authType === 'register') {
<div class="col-12">
<mat-form-field appearance="fill" class="w-100">
<input matInput type="password" placeholder="Confirmez votre nouveau mot de passe" formControlName="confirmPassword" required>
@if (this.authForm.controls['confirmPassword'].status === 'INVALID') {
<mat-error>{{getErrorMessage('confirmPassword')}}</mat-error>
}
</mat-form-field>
</div>
}
<div class="col-12">
<button mat-raised-button color="primary" [disabled]="!authForm.valid" type="submit" class="float-sm-end">{{ btnTitle }}</button>
</div>
</fieldset>
</form>
</div> </div>
</div>
</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();
+24 -16
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,7 +50,7 @@ export class AuthComponent implements OnInit, OnDestroy {
) { ) {
this._resetErrors(); this._resetErrors();
// use FormBuilder to create a form group // use FormBuilder to create a form group
let controlsConfig = { const controlsConfig = {
email: ['', [Validators.required, Validators.email]], email: ['', [Validators.required, Validators.email]],
username: '', username: '',
firstname: '', firstname: '',
@@ -78,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]);
} }
}); });
} }
@@ -93,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,462 +1,259 @@
<div class="calculator-page content"> <div @flyInOut>
<mat-card class="calculator"> <div class="calculator-page content">
<mat-card-header class="bg-warn rounded-top py-3 px-4"> <div class="d-flex">
<mat-card-title>Calcul de taille de voile</mat-card-title> <div class="flex-fill">
<mat-card-subtitle>DT48 - 08 février 2024</mat-card-subtitle> <h1 class="mb-1">{{title}}</h1>
<span class="me-2 fs-6"><span class="fs-5 mx-1">DT48</span> - 08 février 2024</span>
</div>
<span class="flex-spacer"></span> <span class="flex-spacer"></span>
<form #searchFormData="ngForm" class="inputs-form"> <div class="align-self-center me-2">
<mat-form-field class="bg-crystal me-3" subscriptSizing="dynamic"> <button mat-icon-button [matMenuTriggerFor]="menuCalculator" aria-label="Menu Calculator">
<mat-label>Nombre de sauts</mat-label> <mat-icon>more_vert</mat-icon>
<input matInput [(ngModel)]="inputs.jumps" (change)="refresh()" type="number" min="0" max="2000" step="1" name="jumps"> </button>
</mat-form-field> <mat-menu #menuCalculator="matMenu">
<mat-form-field class="bg-crystal me-3" subscriptSizing="dynamic"> @for (menuitem of menuItems.getMenuCalculator(); track menuitem) {
<mat-label>Poids nu</mat-label> @if (menuitem.type === 'link') {
<input matInput [(ngModel)]="inputs.weight" (change)="refresh()" type="number" min="60" max="110" name="weight"> <button mat-menu-item [routerLink]="['/', menuitem.state]">
</mat-form-field>
<mat-form-field class="bg-crystal me-3" subscriptSizing="dynamic">
<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">
<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>
<button mat-icon-button [matMenuTriggerFor]="menuCalculator" aria-label="Menu Calculator">
<mat-icon>more_vert</mat-icon>
</button>
<mat-menu #menuCalculator="matMenu">
<ng-container *ngFor="let menuitem of menuItems.getMenuCalculator()">
<button mat-menu-item [routerLink]="['/', menuitem.state]" *ngIf="menuitem.type === 'link'">
<mat-icon [fontIcon]="menuitem.icon"></mat-icon> <mat-icon [fontIcon]="menuitem.icon"></mat-icon>
<span>{{ menuitem.name }}</span> <span>{{ menuitem.name }}</span>
</button> </button>
</ng-container> }
</mat-menu> }
</mat-card-header> </mat-menu>
<mat-card-content class="my-4"> </div>
<div class="row"> </div>
<div class="col-lg-6 col-md-8 col-sm-12"> <mat-divider class="my-3"></mat-divider>
<div *ngIf="displayCharts" class="linechart position-relative w-100 mt-3"> <mat-card class="calculator">
<canvas baseChart class="mx-auto" width="900" height="350" <mat-card-header class="bg-warn rounded-top pt-1 pb-2 px-2">
[data]="chartConfig.lineChartData" <form #searchFormData="ngForm" class="inputs-form">
[options]="chartConfig.lineChartOptions" <mat-form-field class="bg-crystal me-3" subscriptSizing="dynamic" color="accent">
[legend]="chartConfig.lineChartLegend" <mat-label>Nombre de sauts</mat-label>
[type]="'line'"> <input matInput [(ngModel)]="inputs.jumps" (change)="refresh()" type="number" min="0" max="2000"
</canvas> step="1" name="jumps">
</mat-form-field>
<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="first"></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="first">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="first">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="first">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="first">Coefficient kilo/livres</th>
<td colspan="3" class="font-monospace text-start">{{ coeffKgLbs }}</td>
</tr>
<tr class="warn">
<th class="first">Coefficient pied/mètre</th>
<td colspan="3" class="font-monospace text-start">{{ coeffFtM }}</td>
</tr>
<tr class="warn">
<th class="first">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 class="col-lg-6 col-md-8 col-sm-12">
@if (displayCharts) {
<div class="linechart position-relative w-100 mt-3">
<canvas baseChart class="mx-auto" width="800" height="280"
[data]="chartConfig.lineChartData" [options]="chartConfig.lineChartOptions"
[legend]="chartConfig.lineChartLegend" [type]="'line'">
</canvas>
</div>
}
</div> </div>
</div> </div>
<div class="col-lg-6 col-md-8 col-sm-12"> <mat-divider class="my-3"></mat-divider>
<table class="table table-striped table-dark table-hover mat-elevation-z2"> <h4 class="py-2 mb-0">Tailles de voile pour {{ inputs.weight }} kg</h4>
<thead> <div>
<tr class="warn"> Prochain changement de valeurs dans {{ ((this.currentRange.end + 1) - inputs.jumps) }}
<th class="first"></th> {{ ((this.currentRange.end + 1) - inputs.jumps) > 1 ? 'sauts' : 'saut'}}<br />
<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="first">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="first">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="first">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="first">Coefficient kilo/livres</th>
<td colspan="3" class="font-monospace text-start">{{ coeffKgLbs }}</td>
</tr>
<tr class="warn">
<th class="first">Coefficient pied/mètre</th>
<td colspan="3" class="font-monospace text-start">{{ coeffFtM }}</td>
</tr>
<tr class="warn">
<th class="first">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> <div class="text-end">
<hr /> {{ ((((inputs.jumps - this.currentRange.start) + 1) / ((this.currentRange.end - this.currentRange.start) + 1)) * 100) | number : '1.2-2' }}%
<h4 class="border-bottom border-secondary py-2 mb-2">Tailles de voile pour {{ inputs.weight }} kg</h4> de la tranche actuelle ({{this.currentRange.start}} à {{this.currentRange.end}})
<table class="table table-striped table-dark table-hover mat-elevation-z2 mb-1"> </div>
<thead> <mat-progress-bar mode="determinate"
<tr class="warn"> [value]="((((inputs.jumps - this.currentRange.start) + 1) / ((this.currentRange.end - this.currentRange.start) + 1)) * 100)"
<th class="first"></th> color="warn"></mat-progress-bar>
<th *ngFor="let header of sizesHeader; let index = index;" class="pl-3 pr-3 {{ tableHeader[(index+1)].active }}">{{ header.name }}</th> <table class="table table-striped table-dark table-hover mat-elevation-z2 mb-1">
</tr> <thead>
</thead> <tr class="warn">
<tbody> <th class="first"></th>
<tr class="warn"> @for (header of sizesHeader; track header; let index = $index) {
<th class="first">Taille de voile en ft<sup>2</sup></th> <th class="pl-3 pr-3 {{ tableHeader[(index+1)].active }}">{{ header.name }}</th>
<td *ngFor="let value of sizesValuesFeet; let index = index;" class="pl-3 text-nowrap {{ tableHeader[(index+1)].active }}">{{ value }}</td> }
</tr> </tr>
<tr class="warn"> </thead>
<th class="first">Abbattement 11%</th> <tbody>
<td *ngFor="let value of sizesMinValuesFeet; let index = index;" class="pl-3 text-nowrap {{ tableHeader[(index+1)].active }}">{{ value }}</td> <tr class="warn">
</tr> <th class="first">Taille de voile en ft<sup>2</sup></th>
<tr class="warn"> @for (value of sizesValuesFeet; track value; let index = $index) {
<th class="first">Charge alaire</th> <td class="pl-3 text-nowrap {{ tableHeader[(index+1)].active }}">{{ value }}</td>
<td *ngFor="let charge of sizesCharges; let index = index;" class="pl-3 text-nowrap {{ tableHeader[(index+1)].active }}">{{ charge | number : '1.2-3' }}</td> }
</tr> </tr>
<tr class="warn"> <tr class="warn">
<th class="first">Taille de voile en m<sup>2</sup></th> <th class="first">Abbattement 11%</th>
<td *ngFor="let value of sizesValuesMeter; let index = index;" class="pl-3 text-nowrap {{ tableHeader[(index+1)].active }}">{{ value | number : '1.2-2' }}</td> @for (value of sizesMinValuesFeet; track value; let index = $index) {
</tr> <td class="pl-3 text-nowrap {{ tableHeader[(index+1)].active }}">{{ value }}</td>
</tbody> }
</table> </tr>
<hr /> <tr class="warn">
<h4 class="border-bottom border-secondary py-2 mb-2">Tableau des tailles de voile par poids</h4> <th class="first">Charge alaire</th>
<table mat-table [dataSource]="canopy_size_table" matSort class="table table-striped table-dark table-hover mat-elevation-z2"> @for (charge of sizesCharges; track charge; let index = $index) {
<!-- weight column --> <td class="pl-3 text-nowrap {{ tableHeader[(index+1)].active }}">
<ng-container matColumnDef="weight"> {{ charge | number : '1.2-3' }}</td>
<th mat-header-cell *matHeaderCellDef mat-sort-header class="first {{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)"> </tr>
<span class="text-warn d-block">{{element.weight}}</span> <tr class="warn">
</th> <th class="first">Taille de voile en m<sup>2</sup></th>
</ng-container> @for (value of sizesValuesMeter; track value; let index = $index) {
<!-- range_1 column --> <td class="pl-3 text-nowrap {{ tableHeader[(index+1)].active }}">
<ng-container matColumnDef="range_1"> {{ value | number : '1.2-2' }}</td>
<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> </tr>
</ng-container> </tbody>
<!-- range_2 column --> </table>
<ng-container matColumnDef="range_2"> <mat-divider class="my-3"></mat-divider>
<th class="pl-3" mat-header-cell *matHeaderCellDef class="{{tableHeader[2].active}}"> {{tableHeader[2].name}} </th> <h4 class="py-2 mb-0">Tableau des tailles de voile par poids</h4>
<td mat-cell *matCellDef="let element;" class="pl-3 text-nowrap fs-6 {{element.ranges[1].active}}">{{element.ranges[1].value}}</td> <div class="text-end">
</ng-container> {{ ((inputs.jumps / 1600) * 100) | number : '1.2-2' }}% de la plage complète (0 à 1600)
<!-- range_3 column --> </div>
<ng-container matColumnDef="range_3"> <mat-progress-bar mode="determinate" [value]="((inputs.jumps / 1600) * 100)" color="warn">
<th class="pl-3" mat-header-cell *matHeaderCellDef class="{{tableHeader[3].active}}"> {{tableHeader[3].name}} </th> </mat-progress-bar>
<td mat-cell *matCellDef="let element;" class="pl-3 text-nowrap fs-6 {{element.ranges[2].active}}">{{element.ranges[2].value}}</td> <table mat-table [dataSource]="canopy_size_table" matSort
</ng-container> class="table table-striped table-dark table-hover mat-elevation-z2">
<!-- range_4 column --> <!-- weight column -->
<ng-container matColumnDef="range_4"> <ng-container matColumnDef="weight">
<th class="pl-3" mat-header-cell *matHeaderCellDef class="{{tableHeader[4].active}}"> {{tableHeader[4].name}} </th> <th mat-header-cell *matHeaderCellDef mat-sort-header class="first {{tableHeader[0].active}}">
<td mat-cell *matCellDef="let element;" class="pl-3 text-nowrap fs-6 {{element.ranges[3].active}}">{{element.ranges[3].value}}</td> {{tableHeader[0].name}} </th>
</ng-container> <th mat-cell *matCellDef="let element;" class="text-end ps-3 pe-4 fs-6" role="button"
<!-- range_5 column --> (click)="setCurrentWeight(element.weight)">
<ng-container matColumnDef="range_5"> <span class="text-warn d-block">{{element.weight}}</span>
<th class="pl-3" mat-header-cell *matHeaderCellDef class="{{tableHeader[5].active}}"> {{tableHeader[5].name}} </th> </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>
</ng-container> <!-- range_1 column -->
<!-- range_6 column --> <ng-container matColumnDef="range_1">
<ng-container matColumnDef="range_6"> <th class="pl-3" mat-header-cell *matHeaderCellDef class="{{tableHeader[1].active}}">
<th class="pl-3" mat-header-cell *matHeaderCellDef class="{{tableHeader[6].active}}"> {{tableHeader[6].name}} </th> {{tableHeader[1].name}} </th>
<td mat-cell *matCellDef="let element;" class="pl-3 text-nowrap fs-6 {{element.ranges[5].active}}">{{element.ranges[5].value}}</td> <td mat-cell *matCellDef="let element;"
</ng-container> class="pl-3 text-nowrap fs-6 {{element.ranges[0].active}}">{{element.ranges[0].value}}</td>
<!-- range_7 column --> </ng-container>
<ng-container matColumnDef="range_7"> <!-- range_2 column -->
<th class="pl-3" mat-header-cell *matHeaderCellDef class="{{tableHeader[7].active}}"> {{tableHeader[7].name}} </th> <ng-container matColumnDef="range_2">
<td mat-cell *matCellDef="let element;" class="pl-3 text-nowrap fs-6 {{element.ranges[6].active}}">{{element.ranges[6].value}}</td> <th class="pl-3" mat-header-cell *matHeaderCellDef class="{{tableHeader[2].active}}">
</ng-container> {{tableHeader[2].name}} </th>
<!-- range_8 column --> <td mat-cell *matCellDef="let element;"
<ng-container matColumnDef="range_8"> class="pl-3 text-nowrap fs-6 {{element.ranges[1].active}}">{{element.ranges[1].value}}</td>
<th class="pl-3" mat-header-cell *matHeaderCellDef class="{{tableHeader[8].active}}"> {{tableHeader[8].name}} </th> </ng-container>
<td mat-cell *matCellDef="let element;" class="pl-3 text-nowrap fs-6 {{element.ranges[7].active}}">{{element.ranges[7].value}}</td> <!-- range_3 column -->
</ng-container> <ng-container matColumnDef="range_3">
<!-- range_9 column --> <th class="pl-3" mat-header-cell *matHeaderCellDef class="{{tableHeader[3].active}}">
<ng-container matColumnDef="range_9"> {{tableHeader[3].name}} </th>
<th class="pl-3" mat-header-cell *matHeaderCellDef class="{{tableHeader[9].active}}"> {{tableHeader[9].name}} </th> <td mat-cell *matCellDef="let element;"
<td mat-cell *matCellDef="let element;" class="pl-3 text-nowrap fs-6 {{element.ranges[8].active}}">{{element.ranges[8].value}}</td> class="pl-3 text-nowrap fs-6 {{element.ranges[2].active}}">{{element.ranges[2].value}}</td>
</ng-container> </ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns; sticky: false" class="warn"></tr> <!-- range_4 column -->
<tr mat-row *matRowDef="let row; columns: displayedColumns;" class="text-middle font-monospace warn {{row.active}}" id="weight_{{row.weight}}"></tr> <ng-container matColumnDef="range_4">
</table> <th class="pl-3" mat-header-cell *matHeaderCellDef class="{{tableHeader[4].active}}">
<div class="clearfix"> {{tableHeader[4].name}} </th>
<mat-paginator [pageSize]="50" [pageSizeOptions]="[5, 10, 15, 25, 50]" showFirstLastButtons class="pull-right mb-1 mt-2"></mat-paginator> <td mat-cell *matCellDef="let element;"
</div> class="pl-3 text-nowrap fs-6 {{element.ranges[3].active}}">{{element.ranges[3].value}}</td>
</mat-card-content> </ng-container>
</mat-card> <!-- range_5 column -->
</div> <ng-container matColumnDef="range_5">
<!-- <th class="pl-3" mat-header-cell *matHeaderCellDef class="{{tableHeader[5].active}}">
<div class="content"> {{tableHeader[5].name}} </th>
<h1>{{title}}</h1> <td mat-cell *matCellDef="let element;"
<div class="col-xs-12"> class="pl-3 text-nowrap fs-6 {{element.ranges[4].active}}">{{element.ranges[4].value}}</td>
<div class="demo-buttons my-2"> </ng-container>
<button mat-button color="primary" class="me-2">Texte</button> <!-- range_6 column -->
<button mat-stroked-button color="primary" class="me-2">Texte Stroked</button> <ng-container matColumnDef="range_6">
<button mat-flat-button color="primary" class="me-2">Texte Flat</button> <th class="pl-3" mat-header-cell *matHeaderCellDef class="{{tableHeader[6].active}}">
<button mat-raised-button color="primary" class="me-2">Texte Raised</button> {{tableHeader[6].name}} </th>
<button mat-icon-button color="primary" class="me-2"> <td mat-cell *matCellDef="let element;"
<mat-icon>check_circle</mat-icon> class="pl-3 text-nowrap fs-6 {{element.ranges[5].active}}">{{element.ranges[5].value}}</td>
</button> </ng-container>
<button mat-fab color="primary" class="me-2"> <!-- range_7 column -->
<mat-icon>check_circle</mat-icon> <ng-container matColumnDef="range_7">
</button> <th class="pl-3" mat-header-cell *matHeaderCellDef class="{{tableHeader[7].active}}">
<button mat-mini-fab color="primary" class="me-2"> {{tableHeader[7].name}} </th>
<mat-icon>check_circle</mat-icon> <td mat-cell *matCellDef="let element;"
</button> class="pl-3 text-nowrap fs-6 {{element.ranges[6].active}}">{{element.ranges[6].value}}</td>
</div> </ng-container>
<div class="demo-buttons my-2"> <!-- range_8 column -->
<button mat-button color="accent" class="me-2">Texte</button> <ng-container matColumnDef="range_8">
<button mat-stroked-button color="accent" class="me-2">Texte Stroked</button> <th class="pl-3" mat-header-cell *matHeaderCellDef class="{{tableHeader[8].active}}">
<button mat-flat-button color="accent" class="me-2">Texte Flat</button> {{tableHeader[8].name}} </th>
<button mat-raised-button color="accent" class="me-2">Texte Raised</button> <td mat-cell *matCellDef="let element;"
<button mat-icon-button color="accent" class="me-2"> class="pl-3 text-nowrap fs-6 {{element.ranges[7].active}}">{{element.ranges[7].value}}</td>
<mat-icon>check_circle</mat-icon> </ng-container>
</button> <!-- range_9 column -->
<button mat-fab color="accent" class="me-2"> <ng-container matColumnDef="range_9">
<mat-icon>check_circle</mat-icon> <th class="pl-3" mat-header-cell *matHeaderCellDef class="{{tableHeader[9].active}}">
</button> {{tableHeader[9].name}} </th>
<button mat-mini-fab color="accent" class="me-2"> <td mat-cell *matCellDef="let element;"
<mat-icon>check_circle</mat-icon> class="pl-3 text-nowrap fs-6 {{element.ranges[8].active}}">{{element.ranges[8].value}}</td>
</button> </ng-container>
</div> <tr mat-header-row *matHeaderRowDef="displayedColumns; sticky: false" class="warn"></tr>
<div class="demo-buttons my-2"> <tr mat-row *matRowDef="let row; columns: displayedColumns;"
<button mat-button color="warn" class="me-2">Texte</button> class="text-middle font-monospace warn {{row.active}}" id="weight_{{row.weight}}"></tr>
<button mat-stroked-button color="warn" class="me-2">Texte Stroked</button> </table>
<button mat-flat-button color="warn" class="me-2">Texte Flat</button> <div class="clearfix">
<button mat-raised-button color="warn" class="me-2">Texte Raised</button> <mat-paginator [pageSize]="50" [pageSizeOptions]="[5, 10, 15, 25, 50]" showFirstLastButtons
<button mat-icon-button color="warn" class="me-2"> class="pull-right mb-1 mt-2"></mat-paginator>
<mat-icon>check_circle</mat-icon> </div>
</button> </mat-card-content>
<button mat-fab color="warn" class="me-2"> </mat-card>
<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">
<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">
<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">
<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> </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>
</button>
<button mat-icon-button color="primary" aria-label="Example icon button with a home icon">
<mat-icon>home</mat-icon>
</button>
<button mat-icon-button color="accent" aria-label="Example icon button with a menu icon">
<mat-icon>menu</mat-icon>
</button>
<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>
</div>
</div>
</section>
<mat-divider></mat-divider>
<section>
<div class="demo-label">FAB</div>
<div class="demo-button-row">
<div class="demo-flex-container">
<div class="demo-button-container">
<button mat-fab color="primary" aria-label="Example icon button with a delete icon">
<mat-icon>delete</mat-icon>
</button>
</div>
<div class="demo-button-container">
<button mat-fab color="accent" aria-label="Example icon button with a bookmark icon">
<mat-icon>bookmark</mat-icon>
</button>
</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>
</section>
<mat-divider></mat-divider>
<section>
<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 class="demo-button-container">
<button mat-mini-fab color="accent" aria-label="Example icon button with a plus one icon">
<mat-icon>plus_one</mat-icon>
</button>
</div>
<div class="demo-button-container">
<button mat-mini-fab color="warn" aria-label="Example icon button with a filter list icon">
<mat-icon>filter_list</mat-icon>
</button>
</div>
<div class="demo-button-container">
<button mat-mini-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-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>
-->
@@ -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,7 +1,8 @@
import { Component, Input, OnInit, OnDestroy, ViewChild } 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 { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms';
import { RouterLink, RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card'; import { MatCardModule } from '@angular/material/card';
import { MatDividerModule } from '@angular/material/divider'; import { MatDividerModule } from '@angular/material/divider';
@@ -10,27 +11,41 @@ import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input'; import { MatInputModule } from '@angular/material/input';
import { MatMenuModule } from '@angular/material/menu'; import { MatMenuModule } from '@angular/material/menu';
import { MatPaginator, MatPaginatorModule } from '@angular/material/paginator'; import { MatPaginator, MatPaginatorModule } from '@angular/material/paginator';
import { MatProgressBarModule } from '@angular/material/progress-bar';
import { MatSort, MatSortModule } from '@angular/material/sort'; import { MatSort, MatSortModule } from '@angular/material/sort';
import { MatTableDataSource, MatTableModule } from '@angular/material/table'; import { MatTableDataSource, MatTableModule } from '@angular/material/table';
import { NgChartsModule } from 'ng2-charts'; import { BaseChartDirective } from 'ng2-charts';
import { Observable, Subscription } from 'rxjs'; import { Observable, Subscription } from 'rxjs';
import { MenuItems } from 'src/app/components/shared'; import { MenuItems } from 'src/app/components/shared';
import { CalculatorInfo, CalculatorResult, CalculatorState, InputParams, Jump, LineConfig, Range, TableHeader, User, WeightSize, weightSizes } from 'src/app/core/models'; 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'; import { CalculatorService, JumpsService, UserService, UtilitiesService } from 'src/app/core/services';
@Component({ @Component({
selector: 'app-calculator',
standalone: true, standalone: true,
imports: [ imports: [
CommonModule, FormsModule, RouterModule, CommonModule, FormsModule, RouterModule,
MatButtonModule, MatCardModule, MatDividerModule, MatButtonModule, MatCardModule, MatDividerModule,
MatFormFieldModule, MatInputModule, MatIconModule, MatMenuModule, MatFormFieldModule, MatInputModule, MatIconModule, MatMenuModule,
MatPaginatorModule, MatSortModule, MatTableModule, MatPaginatorModule, MatProgressBarModule, MatSortModule, MatTableModule,
NgChartsModule BaseChartDirective
], ],
selector: 'huapp-calculator', templateUrl: './calculator.component.html',
styleUrls: ['./calculator.component.scss'], styleUrl: './calculator.component.scss',
templateUrl: './calculator.component.html' 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 {
//private _lastjump!: Subscription; // = new Subscription(); //private _lastjump!: Subscription; // = new Subscription();
@@ -39,6 +54,7 @@ export class CalculatorComponent implements OnInit, OnDestroy {
public title = 'Calcul de taille de voile'; public title = 'Calcul de taille de voile';
public subtitle = 'DT48 - 08 février 2024'; public subtitle = 'DT48 - 08 février 2024';
public lastjump: Jump = {} as Jump; public lastjump: Jump = {} as Jump;
public currentRange: WeightSizeRange = {} as WeightSizeRange;
public currentUser: User = {} as User; public currentUser: User = {} as User;
public displayedColumns: Array<string> = ['weight']; public displayedColumns: Array<string> = ['weight'];
public tableHeader: Array<TableHeader> = [<TableHeader>{name: 'Poids nu en kg', active: ''}]; public tableHeader: Array<TableHeader> = [<TableHeader>{name: 'Poids nu en kg', active: ''}];
@@ -81,8 +97,8 @@ export class CalculatorComponent implements OnInit, OnDestroy {
const currentUser$: Observable<User> = this._userService.currentUser; const currentUser$: Observable<User> = this._userService.currentUser;
const lastjump$: Observable<Jump> = this._jumpsService.getLastJump(); const lastjump$: Observable<Jump> = this._jumpsService.getLastJump();
weightSizes[0].ranges.forEach((range: Range) => { weightSizes[0].ranges.forEach((range: WeightSizeRange) => {
let header: TableHeader = {name: range.label, active: range.active}; const header: TableHeader = {name: range.label, active: range.active};
this.tableHeader.push(header) this.tableHeader.push(header)
this.sizesHeader.push(header); this.sizesHeader.push(header);
this.displayedColumns.push(range.name); this.displayedColumns.push(range.name);
@@ -139,6 +155,10 @@ export class CalculatorComponent implements OnInit, OnDestroy {
} }
private _refreshActive(): void { private _refreshActive(): void {
let weight: number = this.inputs.weight;
if (this.inputs.weight < 60) {
weight = 60;
}
weightSizes.forEach(element => { weightSizes.forEach(element => {
element.active = ''; element.active = '';
element.ranges.forEach(range => { element.ranges.forEach(range => {
@@ -148,29 +168,34 @@ export class CalculatorComponent implements OnInit, OnDestroy {
this.tableHeader.forEach(element => { this.tableHeader.forEach(element => {
element.active = ''; element.active = '';
}); });
if (this.inputs.weight >= 60 && this.inputs.weight <= 110) { if (weight >= 60 && weight <= 110) {
weightSizes[(this.inputs.weight - 60)].active = 'active'; weightSizes[(weight - 60)].active = 'active';
} }
if (this.inputs.jumps >= 0) { if (this.inputs.jumps >= 0) {
var num = this._calculatorService.getRangeNum(this.inputs.jumps); const num = this._calculatorService.getRangeNum(this.inputs.jumps);
this.tableHeader[num].active = 'active'; this.tableHeader[num].active = 'active';
weightSizes.forEach(element => { weightSizes.forEach(element => {
element.ranges[(num-1)].active = 'active'; element.ranges[(num-1)].active = 'active';
}); });
this.currentRange = weightSizes[0].ranges[(num-1)];
} }
} }
private _refreshSizes(): void { private _refreshSizes(): void {
let weight: number = this.inputs.weight;
if (this.inputs.weight < 60) {
weight = 60;
}
if (this.inputs.jumps > 2000) { if (this.inputs.jumps > 2000) {
this.info.state.error = '2K+ Jumps'; this.info.state.error = '2K+ Jumps';
} }
this.sizesValuesFeet = this._calculatorService.getCanopySizes(this.inputs.weight); this.sizesValuesFeet = this._calculatorService.getCanopySizes(weight);
this.sizesMinValuesFeet = this._calculatorService.getCanopySizes(this.inputs.weight, true); this.sizesMinValuesFeet = this._calculatorService.getCanopySizes(weight, true);
this.sizesValuesMeter = this.sizesValuesFeet.map((value: number, index: number) => { this.sizesValuesMeter = this.sizesValuesFeet.map((value: number, index: number) => {
this.sizesCharges[index] = this._calculatorService.getCharge(value, this.inputs.weight, this.inputs.gear); this.sizesCharges[index] = this._calculatorService.getCharge(value, this.inputs.weight, this.inputs.gear);
return this._calculatorService.convertFeet2Meters(value); return this._calculatorService.convertFeet2Meters(value);
}); });
this.info.sizesFeet = this._calculatorService.canopySizeCalc(this.inputs.weight, this.inputs.jumps); this.info.sizesFeet = this._calculatorService.canopySizeCalc(weight, this.inputs.jumps);
this.info.sizesFeet.current = this.inputs.current; this.info.sizesFeet.current = this.inputs.current;
this.info.sizesMeter = { this.info.sizesMeter = {
current: this._calculatorService.convertFeet2Meters(this.inputs.current), current: this._calculatorService.convertFeet2Meters(this.inputs.current),
@@ -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,58 @@
<div @flyInOut>
<div class="content">
<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>
</div>
@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { CanopyComponent } from './canopy.component';
describe('CanopyComponent', () => {
let component: CanopyComponent;
let fixture: ComponentFixture<CanopyComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [CanopyComponent]
})
.compileComponents();
fixture = TestBed.createComponent(CanopyComponent);
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-canopy',
standalone: true,
imports: [
DatePipe,
MatButtonModule, MatCardModule, MatDividerModule, MatExpansionModule,
MatGridListModule, MatIconModule, MatMenuModule,
HistoryTableComponent, BarsChartComponent, PieChartComponent
],
templateUrl: './canopy.component.html',
styleUrl: './canopy.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 CanopyComponent 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;
}
}
@@ -1,21 +0,0 @@
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { DashboardComponent } from './dashboard.component';
import { AuthResolver } from 'src/app/core/services';
const routes: Routes = [
{
path: '',
component: DashboardComponent,
resolve: {
isAuthenticated: AuthResolver
}
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class DashboardRoutingModule { }
@@ -1,32 +1,37 @@
<div class="content"> <div class="content">
<h1>{{title}}</h1> <h1 class="mb-1">{{title}}</h1>
<hr /> <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"> <ng-container *appShowAuthed="true">
<div class="row"> <div class="row">
<div class="col-md-8 col-sm-12"> <div class="col-xs-12">
<huapp-jumps-by-month></huapp-jumps-by-month> <app-jumps-by-month></app-jumps-by-month>
</div>
<div class="col-md-4 col-sm-12">
<huapp-drop-zones></huapp-drop-zones>
</div> </div>
</div> </div>
<div class="row mt-sm-2 mt-lg-3"> <div class="row mt-sm-2 mt-lg-3">
<div class="col-lg-3 col-md-4 col-sm-6 col-xs-12"> <div class="col-md-4 col-sm-12">
<huapp-canopy-sizes></huapp-canopy-sizes> <app-drop-zones></app-drop-zones>
</div> </div>
<div class="col-lg-3 col-md-4 col-sm-6 col-xs-12"> <div class="col-md-8 col-sm-12">
<huapp-canopy-models></huapp-canopy-models> <app-drop-zones-bar></app-drop-zones-bar>
<br />
<huapp-aeronefs></huapp-aeronefs>
</div> </div>
<div class="col-lg-6 col-md-4 col-sm-6 col-xs-12"> </div>
<huapp-aeronefs-bar></huapp-aeronefs-bar> <div class="row mt-sm-2 mt-lg-3">
<br /> <div class="col-md-4 col-sm-12">
<huapp-drop-zones-bar></huapp-drop-zones-bar> <app-aeronefs></app-aeronefs>
</div> </div>
<div class="col-lg-3 col-md-4 col-sm-6 col-xs-12"> <div class="col-md-8 col-sm-12">
<app-aeronefs-bar></app-aeronefs-bar>
</div> </div>
<div class="col-lg-3 col-md-4 col-sm-6 col-xs-12"> </div>
<div class="row mt-sm-2 mt-lg-3">
<div class="col-md-6 col-sm-12">
<app-canopy-models></app-canopy-models>
</div>
<div class="col-md-6 col-sm-12">
<app-canopy-sizes></app-canopy-sizes>
</div> </div>
</div> </div>
</ng-container> </ng-container>
@@ -8,8 +8,8 @@ describe('DashboardComponent', () => {
beforeEach(() => { beforeEach(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [DashboardComponent] imports: [DashboardComponent]
}); });
fixture = TestBed.createComponent(DashboardComponent); fixture = TestBed.createComponent(DashboardComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
fixture.detectChanges(); fixture.detectChanges();
@@ -1,13 +1,15 @@
import { Component, OnInit, OnDestroy } from '@angular/core'; import { Component, OnInit, OnDestroy } from '@angular/core';
import { DatePipe } from '@angular/common';
import { Title } from '@angular/platform-browser'; import { Title } from '@angular/platform-browser';
import { MatDividerModule } from '@angular/material/divider';
import { ActivatedRoute, Data, Router } from '@angular/router'; import { ActivatedRoute, Data, Router } from '@angular/router';
import { Observable, Subscription } from 'rxjs'; import { Observable, Subscription } from 'rxjs';
import { take } from 'rxjs/operators'; import { take } from 'rxjs/operators';
import { User, Errors, Aeronef, AeronefByImat, Canopy, CanopyModelBySize, DropZone, DropZoneByYear } from 'src/app/core/models'; 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 { AeronefsService, CanopiesService, DropZonesService, UserService } from 'src/app/core/services';
import { 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 { ShowAuthedDirective } from 'src/app/components/shared/show-authed.directive';
import { import {
AeronefsComponent, AeronefsBarComponent, AeronefsComponent, AeronefsBarComponent,
@@ -19,19 +21,21 @@ import {
@Component({ @Component({
standalone: true, standalone: true,
imports: [ imports: [
ShowAuthedDirective, DatePipe, ShowAuthedDirective, MatDividerModule,
AeronefsComponent, AeronefsBarComponent, AeronefsComponent, AeronefsBarComponent,
CanopyModelsComponent, CanopySizesComponent, CanopyModelsComponent, CanopySizesComponent,
DropZonesComponent, DropZonesBarComponent, DropZonesComponent, DropZonesBarComponent,
JumpsByMonthComponent JumpsByMonthComponent
], ],
selector: 'huapp-dashboard', selector: 'app-dashboard',
templateUrl: './dashboard.component.html', templateUrl: './dashboard.component.html',
styleUrls: ['./dashboard.component.scss'] styleUrl: './dashboard.component.scss'
}) })
export class DashboardComponent implements OnInit, OnDestroy { export class DashboardComponent implements OnInit, OnDestroy {
private _currentUser: Subscription = new Subscription(); private _currentUser: Subscription = new Subscription();
private _data: 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 _aeronefs: Subscription = new Subscription();
private _canopies: Subscription = new Subscription(); private _canopies: Subscription = new Subscription();
@@ -50,12 +54,14 @@ export class DashboardComponent implements OnInit, OnDestroy {
public dropzoneAggregate!: Array<DropZoneByYear>; public dropzoneAggregate!: Array<DropZoneByYear>;
public dropzones!: Array<DropZone>; public dropzones!: Array<DropZone>;
public dropzonesCount = 0; public dropzonesCount = 0;
public lastJump: Jump = {} as Jump;
constructor( constructor(
private route: ActivatedRoute, private route: ActivatedRoute,
private router: Router, private router: Router,
private titleService: Title, private titleService: Title,
private userService: UserService private _userService: UserService,
private _jumpsService: JumpsService
/* /*
private aeronefsService: AeronefsService, private aeronefsService: AeronefsService,
private canopiesService: CanopiesService, private canopiesService: CanopiesService,
@@ -74,10 +80,15 @@ export class DashboardComponent implements OnInit, OnDestroy {
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.canModify = userData.role === 'Admin'; this.canModify = userData.role === 'Admin';
}); });
this._lastjump$ = this._jumpsService.getLastJump();
this._lastjump = this._lastjump$.subscribe((jump) => {
this.lastJump = jump;
});
}); });
/* /*
this._loadAeronefs(); this._loadAeronefs();
@@ -90,6 +101,7 @@ export class DashboardComponent implements OnInit, OnDestroy {
ngOnDestroy() { ngOnDestroy() {
this._currentUser.unsubscribe(); this._currentUser.unsubscribe();
this._data.unsubscribe(); this._data.unsubscribe();
this._lastjump.unsubscribe();
/* /*
this._aeronefs.unsubscribe(); this._aeronefs.unsubscribe();
this._canopies.unsubscribe(); this._canopies.unsubscribe();
@@ -1,15 +0,0 @@
import { NgModule } from '@angular/core';
import { AuthResolver } from 'src/app/core/services';
import { DashboardComponent } from './dashboard.component';
import { DashboardRoutingModule } from './dashboard-routing.module';
@NgModule({
imports: [
DashboardRoutingModule,
DashboardComponent
],
exports: [],
providers: [AuthResolver]
})
export class DashboardModule { }
@@ -0,0 +1,57 @@
<div @flyInOut>
<div class="content">
<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>
</div>
@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { DropzoneComponent } from './dropzone.component';
describe('DropzoneComponent', () => {
let component: DropzoneComponent;
let fixture: ComponentFixture<DropzoneComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [DropzoneComponent]
})
.compileComponents();
fixture = TestBed.createComponent(DropzoneComponent);
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-dropzone',
standalone: true,
imports: [
DatePipe,
MatButtonModule, MatCardModule, MatDividerModule, MatExpansionModule,
MatGridListModule, MatIconModule, MatMenuModule,
//ChartistModule,
HistoryTableComponent, BarsChartComponent, PieChartComponent
],
templateUrl: './dropzone.component.html',
styleUrl: './dropzone.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 DropzoneComponent 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;
}
}
@@ -0,0 +1,26 @@
<div @flyInOut>
<div class="content">
<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">
<p>Home works!</p>
</div>
</div>
<mat-divider class="my-3"></mat-divider>
<app-list-errors [errors]="errors"></app-list-errors>
<mat-card>
<mat-card-header>
<mat-card-title>Card title</mat-card-title>
</mat-card-header>
<mat-card-content class="mt-4">
<p>Home works!</p>
</mat-card-content>
</mat-card>
</div>
</div>
@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { HomeComponent } from './home.component';
describe('HomeComponent', () => {
let component: HomeComponent;
let fixture: ComponentFixture<HomeComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [HomeComponent]
})
.compileComponents();
fixture = TestBed.createComponent(HomeComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
+55
View File
@@ -0,0 +1,55 @@
import { DatePipe } from '@angular/common';
import { trigger, state, style, animate, transition } from '@angular/animations';
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 { ListErrorsComponent, } from 'src/app/components/shared';
import { Errors, Jump } from 'src/app/core/models';
import { JumpsService } from 'src/app/core/services';
@Component({
selector: 'app-home',
standalone: true,
imports: [
DatePipe,
MatCardModule, MatDividerModule,
ListErrorsComponent
],
templateUrl: './home.component.html',
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 {
private _lastjump: Subscription = new Subscription();
private _lastjump$: Observable<Jump> = new Observable();
public title = 'Home';
public errors: Errors = { errors: {} };
public lastJump: Jump = {} as Jump;
public destroyRef = inject(DestroyRef);
constructor(
private _jumpsService: JumpsService
) { }
ngOnInit() {
this._lastjump$ = this._jumpsService.getLastJump().pipe(takeUntilDestroyed(this.destroyRef));
this._lastjump = this._lastjump$.subscribe((jump) => {
this.lastJump = jump;
});
}
}
+6
View File
@@ -1,7 +1,13 @@
export * from './auth/auth.component'; export * from './auth/auth.component';
export * from './aeronef/aeronef.component';
export * from './calculator/calculator.component'; export * from './calculator/calculator.component';
export * from './canopy/canopy.component';
export * from './dashboard/dashboard.component'; export * from './dashboard/dashboard.component';
export * from './dropzone/dropzone.component';
export * from './home/home.component';
export * from './jump/jump.component'; export * from './jump/jump.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';
@@ -1,349 +0,0 @@
<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 />
<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">
<mat-error *ngIf="this.jumpForm.controls['lieu'].status === 'INVALID'">{{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">
<mat-error *ngIf="this.jumpForm.controls['oaci'].status === 'INVALID'">{{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">
<mat-error *ngIf="this.jumpForm.controls['aeronef'].status === 'INVALID'">{{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">
<mat-error *ngIf="this.jumpForm.controls['imat'].status === 'INVALID'">{{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">
<mat-error *ngIf="this.jumpForm.controls['voile'].status === 'INVALID'">{{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" type="number" min="0" step="1">
<span matTextSuffix>ft<sup>2</sup></span>
<mat-error *ngIf="this.jumpForm.controls['taille'].status === 'INVALID'">{{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">
<mat-error *ngIf="this.jumpForm.controls['hauteur'].status === 'INVALID'">{{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">
<mat-error *ngIf="this.jumpForm.controls['deploiement'].status === 'INVALID'">{{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">
<mat-error *ngIf="this.jumpForm.controls['categorie'].status === 'INVALID'">{{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">
<mat-error *ngIf="this.jumpForm.controls['module'].status === 'INVALID'">{{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">
<mat-error *ngIf="this.jumpForm.controls['accessoires'].status === 'INVALID'">{{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">
<mat-error *ngIf="this.jumpForm.controls['zone'].status === 'INVALID'">{{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">
<mat-error *ngIf="this.jumpForm.controls['participants'].status === 'INVALID'">{{getErrorMessage('participants')}}</mat-error>
</mat-form-field>
<div class="mb-2">
<span *ngFor="let sautant of jump.sautants" 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>
ju_solide
</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>
<mat-error *ngIf="this.jumpForm.controls['programme'].status === 'INVALID'">{{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">
<mat-error *ngIf="this.jumpForm.controls['dossier'].status === 'INVALID'">{{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">
<mat-error *ngIf="this.jumpForm.controls['video'].status === 'INVALID'">{{getErrorMessage('video')}}</mat-error>
</mat-form-field>
<span class="d-inline-block align-middle ms-1">{{ filepath }}{{ filename }}</span>
</div>
</form>
<div class="row" [hidden]="true">
<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)" [(ngModel)]="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 [(ngModel)]="jump.lieu">
</mat-form-field>
<mat-form-field appearance="fill" class="me-2" color="warn">
<mat-label>Oaci</mat-label>
<input matInput [(ngModel)]="jump.oaci">
</mat-form-field>
<h4 class="border-bottom border-secondary mb-3 lh-lg">
Aéronef
<mat-checkbox class="float-end" (change)="setLastAeronef($event.checked)" [(ngModel)]="useLast.aeronef">
<mat-icon fontIcon="history" class="align-middle"></mat-icon>
</mat-checkbox>
</h4>
<mat-form-field appearance="fill" class="me-2">
<mat-label>Aéronef</mat-label>
<input matInput [(ngModel)]="jump.aeronef">
</mat-form-field>
<mat-form-field appearance="fill" class="me-2">
<mat-label>Imatriculation</mat-label>
<input matInput [(ngModel)]="jump.imat">
</mat-form-field>
<h4 class="border-bottom border-secondary mb-3 lh-lg">
Voile
<mat-checkbox class="float-end" (change)="setLastVoile($event.checked)" [(ngModel)]="useLast.voile">
<mat-icon fontIcon="history" class="align-middle"></mat-icon>
</mat-checkbox>
</h4>
<mat-form-field appearance="fill" class="me-2">
<mat-label>Modèle</mat-label>
<input matInput [(ngModel)]="jump.voile">
</mat-form-field>
<mat-form-field appearance="fill" class="me-2">
<mat-label>Taille</mat-label>
<input matInput [(ngModel)]="jump.taille" type="number" min="0" step="1">
</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)" [(ngModel)]="useLast.altitude">
<mat-icon fontIcon="history" class="align-middle"></mat-icon>
</mat-checkbox>
</h4>
<mat-form-field appearance="fill" class="me-2">
<mat-label>Hauteur</mat-label>
<input matInput [(ngModel)]="jump.hauteur" type="number" min="0" step="1">
</mat-form-field>
<mat-form-field appearance="fill" class="me-2">
<mat-label>Hauteur de déploiement</mat-label>
<input matInput [(ngModel)]="jump.deploiement" type="number" min="0" step="1">
</mat-form-field>
<h4 class="border-bottom border-secondary mb-3 lh-lg">
Discipline
<mat-checkbox class="float-end" (change)="setLastDiscipline($event.checked)" [(ngModel)]="useLast.discipline">
<mat-icon fontIcon="history" class="align-middle"></mat-icon>
</mat-checkbox>
</h4>
<mat-form-field appearance="fill" class="me-2">
<mat-label>Categorie</mat-label>
<input matInput [(ngModel)]="jump.categorie">
<mat-hint>FF / Solo / VR / Wingsuit</mat-hint>
</mat-form-field>
<mat-form-field appearance="fill" class="me-2">
<mat-label>Module</mat-label>
<input matInput [(ngModel)]="jump.module">
<mat-hint>Chutas / Track / Trace</mat-hint>
</mat-form-field>
<h4 class="border-bottom border-secondary mb-3 lh-lg">
Divers
<mat-checkbox class="float-end" (change)="setLastDivers($event.checked)" [(ngModel)]="useLast.divers">
<mat-icon fontIcon="history" class="align-middle"></mat-icon>
</mat-checkbox>
</h4>
<mat-form-field appearance="fill" class="me-2">
<mat-label>Accessoires</mat-label>
<input matInput [(ngModel)]="jump.accessoires">
</mat-form-field>
<mat-form-field appearance="fill" class="me-2">
<mat-label>Zone</mat-label>
<input matInput [(ngModel)]="jump.zone">
</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)" [(ngModel)]="useLast.groupe">
<mat-icon fontIcon="history" class="align-middle"></mat-icon>
</mat-checkbox>
</h4>
<mat-form-field appearance="fill" class="me-2">
<mat-label>Sautants</mat-label>
<input matInput type="text" placehomder="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 [(ngModel)]="jump.participants" type="number" min="0" step="1" [value]="jump.sautants.length+1">
</mat-form-field>
<div class="mb-2">
<span *ngFor="let sautant of jump.sautants" 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>
ju_solide
</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" [(ngModel)]="params.video">
<mat-icon fontIcon="videocam" class="align-middle"></mat-icon>
</mat-checkbox>
</h4>
<mat-form-field appearance="fill" class="me-2 w-100">
<mat-label>Programme</mat-label>
<textarea matInput [(ngModel)]="jump.programme"></textarea>
</mat-form-field>
<span class="d-inline-block align-middle ms-1">{{ filepath }}{{ this.jumpForm.controls['date'].value | date: 'yyyy-MM-dd' }}_S00{{ nextJump }}.mp4</span>
<span class="d-inline-block align-middle ms-1">{{ jump.dossier }}{{ jump.video }}</span>
</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="warn" [disabled]="!jumpForm.valid" [mat-dialog-close]="{jump: jump, params: params}" cdkFocusInitial>Enregistrer</button>-->
<button mat-button color="warn" [disabled]="!jumpForm.valid" (click)="submitForm()" cdkFocusInitial>Enregistrer</button>
</mat-dialog-actions>
@@ -1,145 +0,0 @@
<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">
<mat-error *ngIf="this.jumpForm.controls['lieu'].status === 'INVALID'">{{getErrorMessage('lieu')}}</mat-error>
</mat-form-field>
<mat-form-field appearance="fill" class="me-2">
<mat-label>Oaci</mat-label>
<input matInput formControlName="oaci">
<mat-error *ngIf="this.jumpForm.controls['oaci'].status === 'INVALID'">{{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">
<mat-error *ngIf="this.jumpForm.controls['aeronef'].status === 'INVALID'">{{getErrorMessage('aeronef')}}</mat-error>
</mat-form-field>
<mat-form-field appearance="fill" class="me-2">
<mat-label>Imatriculation</mat-label>
<input matInput formControlName="imat">
<mat-error *ngIf="this.jumpForm.controls['imat'].status === 'INVALID'">{{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">
<mat-error *ngIf="this.jumpForm.controls['voile'].status === 'INVALID'">{{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" type="number" min="0" step="1">
<span matTextSuffix>ft<sup>2</sup></span>
<mat-error *ngIf="this.jumpForm.controls['taille'].status === 'INVALID'">{{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">
<mat-error *ngIf="this.jumpForm.controls['hauteur'].status === 'INVALID'">{{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">
<mat-error *ngIf="this.jumpForm.controls['deploiement'].status === 'INVALID'">{{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">
<mat-error *ngIf="this.jumpForm.controls['categorie'].status === 'INVALID'">{{getErrorMessage('categorie')}}</mat-error>
</mat-form-field>
<mat-form-field appearance="fill" class="me-2">
<mat-label>Module</mat-label>
<input matInput formControlName="module">
<mat-error *ngIf="this.jumpForm.controls['module'].status === 'INVALID'">{{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">
<mat-error *ngIf="this.jumpForm.controls['accessoires'].status === 'INVALID'">{{getErrorMessage('accessoires')}}</mat-error>
</mat-form-field>
<mat-form-field appearance="fill" class="me-2">
<mat-label>Zone</mat-label>
<input matInput formControlName="zone">
<mat-error *ngIf="this.jumpForm.controls['zone'].status === 'INVALID'">{{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">
<mat-error *ngIf="this.jumpForm.controls['participants'].status === 'INVALID'">{{getErrorMessage('participants')}}</mat-error>
</mat-form-field>
<div class="mb-2">
<span *ngFor="let sautant of jump.sautants" 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>
ju_solide
</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>
<mat-error *ngIf="this.jumpForm.controls['programme'].status === 'INVALID'">{{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">
<mat-error *ngIf="this.jumpForm.controls['dossier'].status === 'INVALID'">{{getErrorMessage('dossier')}}</mat-error>
</mat-form-field>
<mat-form-field appearance="fill" class="me-2">
<mat-label>Video</mat-label>
<input matInput formControlName="video">
<mat-error *ngIf="this.jumpForm.controls['video'].status === 'INVALID'">{{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>
@@ -1,98 +0,0 @@
import { Component, Inject } from '@angular/core';
import { NgFor, NgIf } from '@angular/common';
import { FormBuilder, FormControl, FormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms';
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 { Jump, JumpForm } from 'src/app/core/models';
@Component({
selector: 'huapp-jump-edit-dialog',
templateUrl: 'jump-edit.dialog.html',
standalone: true,
imports: [
NgFor, NgIf, FormsModule, ReactiveFormsModule,
MatButtonModule, MatCheckboxModule, MatDatepickerModule,
MatDialogModule, MatFormFieldModule, MatIconModule,
MatInputModule, MatSelectModule, MatOptionModule
]
})
export class JumpEditDialogComponent {
public tagField = new FormControl<string>('', { nonNullable: true});
public jumpForm: FormGroup;
constructor(
public dialogRef: MatDialogRef<JumpEditDialogComponent>,
private fb: FormBuilder,
@Inject(MAT_DIALOG_DATA) public jump: Jump
) {
let 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, 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);
}
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();
}
getErrorMessage(name: string): string {
if (this.jumpForm.controls[name].errors !== null) {
return `Ce champ est requis.`;
}
return '';
}
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);
// close the dialog with result
this.dialogRef.close(this.jump);
}
updateJump(values: NonNullable<unknown>): void {
Object.assign(this.jump, values);
}
}
@@ -1,67 +0,0 @@
<h2 mat-dialog-title class="bg-cyan 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">
<h5 class="border-bottom text-light-info border-light-subtle pb-2 mb-3">Le {{ jump.date | date: 'dd/MM/yyyy' }} à {{ jump.lieu }} :</h5>
<div class="row mb-3">
<div class="col-lg-6 col-md-12">
<dl class="dl-horizontal">
<dt class="text-cyan fw-normal">Lieu:</dt>
<dd>{{jump.lieu}}</dd>
<dt class="text-cyan fw-normal">Oaci:</dt>
<dd>{{jump.oaci}}</dd>
<dt class="text-cyan fw-normal">Aeronef:</dt>
<dd>{{jump.aeronef}}</dd>
<dt class="text-cyan fw-normal">Imat:</dt>
<dd>{{jump.imat}}</dd>
<dt class="text-cyan fw-normal">Voile:</dt>
<dd>{{jump.voile}}</dd>
<dt class="text-cyan fw-normal">Taille:</dt>
<dd>{{jump.taille}} ft<sup>2</sup></dd>
<dt class="text-cyan fw-normal">Participants:</dt>
<dd>{{jump.participants}}</dd>
<dt class="text-cyan fw-normal">Sautants:</dt>
<dd>
<span *ngFor="let sautant of jump.sautants" 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>
ju_solide
</span>
</dd>
</dl>
</div>
<div class="col-lg-6 col-md-12">
<dl class="dl-horizontal">
<dt class="text-cyan fw-normal">Hauteur:</dt>
<dd>{{jump.hauteur}} m</dd>
<dt class="text-cyan fw-normal">Deploiement:</dt>
<dd>{{jump.deploiement}} m</dd>
<dt class="text-cyan fw-normal">Categorie:</dt>
<dd>{{jump.categorie}}</dd>
<dt class="text-cyan fw-normal">Module:</dt>
<dd>{{jump.module}}</dd>
<dt class="text-cyan fw-normal">Accessoires:</dt>
<dd>{{jump.accessoires}}</dd>
<dt class="text-cyan fw-normal">Zone:</dt>
<dd>{{jump.zone}}</dd>
<dt class="text-cyan fw-normal">Dossier media:</dt>
<dd>{{jump.dossier}}</dd>
<dt class="text-cyan fw-normal">Video:</dt>
<dd>{{jump.video}}</dd>
<dt class="text-cyan fw-normal">Programme:</dt>
<dd>{{jump.programme}}</dd>
</dl>
</div>
</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>
@@ -1,24 +0,0 @@
import { Injectable, } from '@angular/core';
import { ActivatedRouteSnapshot, Resolve, Router, RouterStateSnapshot } from '@angular/router';
import { Observable } from 'rxjs';
import { Jump } from 'src/app/core/models';
import { JumpsService } from 'src/app/core/services';
import { catchError } from 'rxjs/operators';
@Injectable()
export class JumpResolver implements Resolve<Jump> {
constructor(
private _jumpsService: JumpsService,
private router: Router
) { }
resolve(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): Observable<any> {
return this._jumpsService.get(route.params['slug'])
.pipe(catchError(() => this.router.navigateByUrl('/')));
}
}
@@ -1,22 +0,0 @@
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { JumpComponent } from './jump.component';
import { JumpResolver } from './jump-resolver.service';
import { AuthGuard } from 'src/app/core/services';
const routes: Routes = [
{
path: ':slug',
component: JumpComponent,
canActivate: [AuthGuard],
resolve: {
jump: JumpResolver
}
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class JumpRoutingModule { }
+131 -72
View File
@@ -1,73 +1,132 @@
<div class="content"> <div @flyInOut>
<h1 class="my-0">Saut n°{{ jump.numero }} <small class="text-muted">le {{ jump.date | date: 'dd/MM/yyyy' }} à {{ jump.lieu }}</small></h1> <div class="content">
<hr /> <div class="d-flex">
<section class="row mb-3"> <div class="flex-fill">
<div class="col-lg-6 col-md-12"> <h1 class="mb-1">{{title}}</h1>
<dl class="dl-horizontal"> <span [hidden]="!lastJump.numero" class="me-2 fs-6">
<dt class="text-accent fw-normal">Lieu:</dt> 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>
<dd>{{jump.lieu}}</dd> </span>
<dt class="text-accent fw-normal">Oaci:</dt> </div>
<dd>{{jump.oaci}}</dd> <span class="flex-spacer"></span>
<dt class="text-accent fw-normal">Aeronef:</dt> <div class="align-self-center me-2">
<dd>{{jump.aeronef}}</dd> <button mat-icon-button [matMenuTriggerFor]="menuAeronef" aria-label="Menu Aeronef">
<dt class="text-accent fw-normal">Imat:</dt> <mat-icon fontIcon="more_vert"></mat-icon>
<dd>{{jump.imat}}</dd> </button>
<dt class="text-accent fw-normal">Voile:</dt> <mat-menu #menuAeronef="matMenu">
<dd>{{jump.voile}}</dd> @for (menuitem of menuItems.getMenuPanel(); track menuitem) {
<dt class="text-accent fw-normal">Taille:</dt> <button mat-menu-item>
<dd>{{jump.taille}} ft<sup>2</sup></dd> <mat-icon [fontIcon]="menuitem.icon"></mat-icon>
<dt class="text-accent fw-normal">Programme:</dt> <span>{{ menuitem.name }}</span>
<dd>{{jump.programme}}</dd> </button>
<dt class="text-accent fw-normal">Participants:</dt> }
<dd>{{jump.participants}}</dd> </mat-menu>
</dl> </div>
</div> </div>
<div class="col-lg-6 col-md-12"> <mat-divider class="my-3"></mat-divider>
<dl class="dl-horizontal"> <mat-card>
<dt class="text-accent fw-normal">Hauteur:</dt> <mat-card-header>
<dd>{{jump.hauteur}} m</dd> <mat-card-title>{{ subtitle }}</mat-card-title>
<dt class="text-accent fw-normal">Deploiement:</dt> </mat-card-header>
<dd>{{jump.deploiement}} m</dd> <mat-card-content class="mt-2">
<dt class="text-accent fw-normal">Categorie:</dt> @if(displayCharts) {
<dd>{{jump.categorie}}</dd> <div class="row">
<dt class="text-accent fw-normal">Module:</dt> <div class="col-xxl-6 col-xs-12">
<dd>{{jump.module}}</dd> <app-linearea-chart [headers]="seriesHeader" [names]="seriesName" [values]="seriesRow" [colors]="seriesColor"></app-linearea-chart>
<dt class="text-accent fw-normal">Accessoires:</dt> </div>
<dd>{{jump.accessoires}}</dd> <div class="col-xxl-6 col-xs-12">
<dt class="text-accent fw-normal">Zone:</dt> <app-bars-chart [headers]="seriesHeader" [names]="seriesName" [values]="seriesRow" [colors]="seriesColor" [legend]="true"></app-bars-chart>
<dd>{{jump.zone}}</dd> </div>
<dt class="text-accent fw-normal">Emplacement des media:</dt> </div>
<dd>{{jump.dossier}}</dd> }
<dt class="text-accent fw-normal">Video:</dt> <div class="row">
<dd>{{jump.video}}</dd> <div class="col-xs-12">
</dl> <mat-divider class="mt-3 mb-0"></mat-divider>
</div> <table class="table table-striped table-dark table-hover">
<div class="col-xs-12 mb-2"> <thead>
<span *ngFor="let sautant of jump.sautants" class="badge rounded-pill bg-primary mb-2 me-1 pe-3 py-1 fw-normal text-dark"> <tr>
<mat-icon class="align-middle me-1" fontIcon="person"></mat-icon> <th></th>
{{ sautant }} <th *ngFor='let name of seriesHeader; let i=index' class="text-end"> {{ name }} </th>
</span> <th class="text-end">Total</th>
<span class="badge rounded-pill bg-accent mb-2 me-1 pe-3 py-1 fw-normal text-dark"> </tr>
<mat-icon class="align-middle me-1" fontIcon="person"></mat-icon> </thead>
ju_solide <tbody>
</span> <tr *ngFor='let values of seriesRow; let i=index'>
</div> <th class="{{seriesColorClass[i]}} text-end"> ● {{ seriesName[i] }} </th>
</section> <td *ngFor='let value of values; let i=index' class="font-monospace text-end">
<section class="border-top pt-4 d-flex"> <span *ngIf="value; else elseBlock" class="pr-1">{{ value }}</span>
<!-- <ng-template #elseBlock><span class="text-empty pr-1">{{ value }}</span></ng-template>
<huapp-jump-meta [jump]="jump" class="me-2"></huapp-jump-meta> </td>
<span class="flex-spacer"></span> <th class="{{seriesColorClass[i]}} font-monospace text-end">{{ seriesRowTotal[i] }}</th>
--> </tr>
<span class="flex-spacer"></span> </tbody>
<span [hidden]="!canModify"> <tfoot>
<button mat-button color="primary" (click)="openEditDialog(jump)" class="me-1"> <tr>
<mat-icon aria-label="Modifier" fontIcon="edit"></mat-icon> <th class="text-end">Total</th>
Modifier <th *ngFor='let value of seriesColTotal; let i=index' class="font-monospace text-end">
</button> <span *ngIf="value; else elseBlock" class="pr-1">{{ value }}</span>
<button mat-button color="danger" (click)="openDeleteDialog(jump)"> <ng-template #elseBlock><span class="text-empty pr-1">{{ value }}</span></ng-template>
<mat-icon aria-label="Supprimer" fontIcon="delete"></mat-icon> </th>
Supprimer <th class="font-monospace text-end">{{ grandTotal }}</th>
</button> </tr>
</span> <tr>
</section> <th class="text-end">Moyenne</th>
</div> <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>
<mat-card class="mt-3">
<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"></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>
</div>
</div>
@@ -8,8 +8,8 @@ describe('JumpComponent', () => {
beforeEach(() => { beforeEach(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [JumpComponent] imports: [JumpComponent]
}); });
fixture = TestBed.createComponent(JumpComponent); fixture = TestBed.createComponent(JumpComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
fixture.detectChanges(); fixture.detectChanges();
+216 -107
View File
@@ -1,139 +1,248 @@
import { Component, OnInit, OnDestroy } from '@angular/core'; 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 { CommonModule } from '@angular/common';
import { ActivatedRoute, Router, RouterModule } from '@angular/router';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatNativeDateModule } from '@angular/material/core'; import { MatCardModule } from '@angular/material/card';
import { MatDialog, MatDialogModule } from '@angular/material/dialog'; import { MatDividerModule } from '@angular/material/divider';
import { MatExpansionModule } from '@angular/material/expansion';
import { MatIconModule } from '@angular/material/icon'; import { MatIconModule } from '@angular/material/icon';
import { MatSnackBar, MatSnackBarHorizontalPosition, MatSnackBarModule, MatSnackBarVerticalPosition } from '@angular/material/snack-bar'; import { MatMenuModule } from '@angular/material/menu';
import { Observable, Subscription } from 'rxjs';
import { take } from 'rxjs/operators';
import { Errors, Jump, User } from 'src/app/core/models'; import { Observable, Subscription } from 'rxjs';
import { JumpsService, UserService } from 'src/app/core/services';
import { JumpDeleteDialogComponent, JumpEditDialogComponent } from 'src/app/components/jump/dialogs' 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({ @Component({
selector: 'app-jump',
standalone: true, standalone: true,
imports: [ imports: [
CommonModule, RouterModule, CommonModule,
MatButtonModule, MatDialogModule, MatIconModule, MatNativeDateModule, MatSnackBarModule, MatButtonModule, MatCardModule, MatDividerModule,
JumpDeleteDialogComponent, JumpEditDialogComponent MatExpansionModule, MatIconModule, MatMenuModule,
BarsChartComponent, CircleChartComponent, LineChartComponent, LineAreaChartComponent
], ],
selector: 'huapp-jump',
templateUrl: './jump.component.html', templateUrl: './jump.component.html',
styleUrls: ['./jump.component.scss'] styleUrl: './jump.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 JumpComponent implements OnInit, OnDestroy { export class JumpComponent implements OnInit, OnDestroy {
private _currentUser: Subscription = new Subscription();
private _data: Subscription = new Subscription(); private _data: Subscription = new Subscription();
public errors!: Errors; private _jumpsByCategorie: Array<JumpByCategorie> = [];
public jump: Jump = {} as Jump; private _jumpsByModule: Array<JumpByModule> = [];
public currentUser: User = {} as User; private _jumpsByDate: Array<JumpByDate> = [];
public canModify = false; 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( constructor(
private route: ActivatedRoute, private route: ActivatedRoute,
private router: Router, private _utilitiesService: UtilitiesService,
private dialog: MatDialog, public menuItems: MenuItems
private _jumpsService: JumpsService,
private _userService: UserService,
private _snackBar: MatSnackBar
) { } ) { }
ngOnInit() { ngOnInit() {
// Retreive the prefetched jump const data$: Observable<{ jumpsPageData: JumpsPageData }> = this.route.data as Observable<{ jumpsPageData: JumpsPageData }>;
const data$: Observable<any> = this.route.data; this._data = data$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((data: { jumpsPageData: JumpsPageData }) => {
this._data = data$.subscribe((data: { jump: Jump }) => { const pageData: JumpsPageData = data.jumpsPageData;
this.jump = data.jump; this.lastJump = pageData.lastjump;
}); const currentYear: number = new Date().getFullYear();
// Load the current user's data const types: Array<string> = ['FF', 'Solo', 'VR', 'Wingsuit'];
const currentUser$: Observable<User> = this._userService.currentUser; const verticals: Array<string> = ['Chutas', 'Head Up', 'Head Down'];
this._currentUser = currentUser$.subscribe((userData: User) => { const others: Array<string> = ['Track', 'Trace'];
this.currentUser = userData; const modules: Array<string> = [...verticals, ...others];
this.canModify = this.currentUser.username === this.jump.author.username; 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-2)); // -2 pour ne pas compter la première année
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() { ngOnDestroy() {
this._data.unsubscribe(); this._data.unsubscribe();
this._currentUser.unsubscribe();
} }
openDeleteDialog(jump: Jump): void { public getCategories(): Array<JumpByCategorie> {
const dialogRef = this.dialog.open(JumpDeleteDialogComponent, { return this._jumpsByCategorie;
width: '60vw',
data: jump
});
dialogRef.afterClosed().pipe(take(1))
.subscribe(result => {
if (result != undefined) {
this._onDelete(result.slug);
}
});
} }
openEditDialog(jump: Jump): void { public getModules(): Array<JumpByModule> {
const dialogRef = this.dialog.open(JumpEditDialogComponent, { return this._jumpsByModule;
width: '70vw',
data: jump
});
dialogRef.afterClosed().pipe(take(1))
.subscribe(result => {
if (result != undefined) {
this._onEdit(result);
}
});
} }
private _onDelete(slug: string): void { public getModuleColor(index: number): number {
try { return (this._jumpsByCategorie.length + index);
this._jumpsService.destroy(slug)
.pipe(take(1))
.subscribe({
next: () => {
this.router.navigateByUrl('/logbook');
},
error: (err) => {
this.errors = err;
}
});
} catch (error) {
console.error(error);
}
} }
private _onEdit(jump: Jump): void {
try {
let previousValues: Jump = {} as Jump;
Object.assign(previousValues, this.jump);
this._jumpsService.update(jump)
.pipe(take(1))
.subscribe({
next: (jump) => {
this._resetErrors();
this._openSnackBar(`Le saut n°${jump.numero} a été mis à jour !`, 'Annuler');
this.jump = jump;
},
error: (err) => {
this.errors = err;
}
});
} catch (error) {
console.error(error);
}
}
private _openSnackBar(content: string, title: string) {
this._snackBar.open(content, title, {
horizontalPosition: <MatSnackBarHorizontalPosition>'end',
verticalPosition: <MatSnackBarVerticalPosition>'bottom',
duration: 4000,
});
}
private _resetErrors(): void {
this.errors = { errors: {} };
}
} }
-19
View File
@@ -1,19 +0,0 @@
import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { JumpComponent } from './jump.component';
import { JumpResolver } from './jump-resolver.service';
import { JumpRoutingModule } from './jump-routing.module';
@NgModule({
imports: [
JumpRoutingModule,
JumpComponent
],
providers: [
JumpResolver
],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class JumpModule { }
@@ -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>
ju_solide
</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>
@@ -1,6 +1,7 @@
import { Component, Inject, OnDestroy } from '@angular/core'; import { Component, Inject, OnDestroy } from '@angular/core';
import { DatePipe, NgFor, NgIf } from '@angular/common'; import { AsyncPipe, DatePipe } from '@angular/common';
import { FormBuilder, FormControl, FormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms'; import { FormBuilder, FormControl, FormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms';
import { MatAutocompleteModule } from '@angular/material/autocomplete';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatCheckboxModule } from '@angular/material/checkbox'; import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatDatepickerModule } from '@angular/material/datepicker'; import { MatDatepickerModule } from '@angular/material/datepicker';
@@ -10,23 +11,39 @@ import { MatInputModule } from '@angular/material/input';
import { MatFormFieldModule } from '@angular/material/form-field'; import { MatFormFieldModule } from '@angular/material/form-field';
import { MatOptionModule } from '@angular/material/core'; import { MatOptionModule } from '@angular/material/core';
import { MatSelectModule } from '@angular/material/select'; import { MatSelectModule } from '@angular/material/select';
import { Subscription } from 'rxjs'; import { Observable, Subscription } from 'rxjs';
import { map, startWith } from 'rxjs/operators';
import { Jump, JumpAddParams } from 'src/app/core/models'; import { AeronefByImat, CanopyModelBySize, DropZoneByOaci, Jump, JumpAddParams, JumpByModule } from 'src/app/core/models';
import { AeronefsService, CanopiesService, DropZonesService, JumpsService } from 'src/app/core/services';
@Component({ @Component({
selector: 'huapp-jump-add-dialog', selector: 'app-jump-add-dialog',
templateUrl: 'jump-add.dialog.html', templateUrl: 'jump-add.dialog.html',
standalone: true, standalone: true,
imports: [ imports: [
DatePipe, NgFor, NgIf, FormsModule, ReactiveFormsModule, AsyncPipe,
MatButtonModule, MatCheckboxModule, MatDatepickerModule, DatePipe,
MatDialogModule, MatFormFieldModule, MatIconModule, FormsModule,
MatInputModule, MatSelectModule, MatOptionModule ReactiveFormsModule,
] MatAutocompleteModule,
MatButtonModule,
MatCheckboxModule,
MatDatepickerModule,
MatDialogModule,
MatFormFieldModule,
MatIconModule,
MatInputModule,
MatSelectModule,
MatOptionModule
]
}) })
export class JumpAddDialogComponent implements OnDestroy { export class JumpAddDialogComponent implements OnDestroy {
private _formChanges: Array<Subscription> = []; 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 tagField = new FormControl<string>('', { nonNullable: true});
public jumpForm: FormGroup; public jumpForm: FormGroup;
public jump: Jump; public jump: Jump;
@@ -56,12 +73,34 @@ export class JumpAddDialogComponent implements OnDestroy {
programme: false, programme: false,
voile: 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( constructor(
public dialogRef: MatDialogRef<JumpAddDialogComponent>, public dialogRef: MatDialogRef<JumpAddDialogComponent>,
private fb: FormBuilder, 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} @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.jump = data.jump;
this.lastJump = data.lastJump; this.lastJump = data.lastJump;
this.nextJump = (this.lastJump.numero + 1); this.nextJump = (this.lastJump.numero + 1);
@@ -71,7 +110,7 @@ export class JumpAddDialogComponent implements OnDestroy {
video: true video: true
}; };
this.jump.numero = this.nextJump; this.jump.numero = this.nextJump;
let controlsConfig = { const controlsConfig = {
date: ['', Validators.required], date: ['', Validators.required],
numero: [this.nextJump, Validators.required], numero: [this.nextJump, Validators.required],
lieu: ['', Validators.required], lieu: ['', Validators.required],
@@ -103,12 +142,95 @@ export class JumpAddDialogComponent implements OnDestroy {
this.filename = this._computeFilename(); 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() { ngOnDestroy() {
this._formChanges.forEach((sub: Subscription) => { this._formChanges.forEach((sub: Subscription) => {
sub.unsubscribe(); sub.unsubscribe();
}); });
this._aeronefByImat.unsubscribe();
this._canopyModelBySize.unsubscribe();
this._dropZoneByOaci.unsubscribe();
this._jumpByModule.unsubscribe();
} }
public onDateChange(): void { public onDateChange(): void {
@@ -117,9 +239,9 @@ export class JumpAddDialogComponent implements OnDestroy {
private _computeFilename(numero?: string): string { private _computeFilename(numero?: string): string {
let file: string = ''; let file: string = '';
let timestamp: number = Date.parse(this.jumpForm.controls['date'].value); const timestamp: number = Date.parse(this.jumpForm.controls['date'].value);
if (isNaN(timestamp) == false) { if (isNaN(timestamp) == false) {
let date: Date = new Date(timestamp); const date: Date = new Date(timestamp);
file += `${(date.getFullYear() + '').padStart(2, '0')}-${((date.getMonth() + 1) + '').padStart(2, '0')}-${(date.getDate() + '').padStart(2, '0')}_`; file += `${(date.getFullYear() + '').padStart(2, '0')}-${((date.getMonth() + 1) + '').padStart(2, '0')}-${(date.getDate() + '').padStart(2, '0')}_`;
} }
if (numero !== undefined) { if (numero !== undefined) {
@@ -156,6 +278,78 @@ export class JumpAddDialogComponent implements OnDestroy {
); );
} }
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) { private _setAllLast(checked: boolean) {
this.useLast.all = checked; this.useLast.all = checked;
this.useLast.aeronef = checked; this.useLast.aeronef = checked;
@@ -187,6 +381,106 @@ export class JumpAddDialogComponent implements OnDestroy {
this.dialogRef.close(); 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 { getErrorMessage(name: string): string {
if (this.jumpForm.controls[name].errors !== null) { if (this.jumpForm.controls[name].errors !== null) {
return 'Ce champ est requis.'; return 'Ce champ est requis.';
@@ -194,6 +488,60 @@ export class JumpAddDialogComponent implements OnDestroy {
return ''; 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 { removeSautant(index: string): void {
this.useLast.groupe = false; this.useLast.groupe = false;
this.useLast.all = this._isLastForAllSections(); this.useLast.all = this._isLastForAllSections();
@@ -212,7 +560,7 @@ export class JumpAddDialogComponent implements OnDestroy {
this.jumpForm.controls['hauteur'].setValue(this.lastJump.hauteur); this.jumpForm.controls['hauteur'].setValue(this.lastJump.hauteur);
this.jumpForm.controls['deploiement'].setValue(this.lastJump.deploiement); this.jumpForm.controls['deploiement'].setValue(this.lastJump.deploiement);
this.jumpForm.controls['voile'].setValue(this.lastJump.voile); this.jumpForm.controls['voile'].setValue(this.lastJump.voile);
this.jumpForm.controls['taille'].setValue(this.lastJump.taille); this.jumpForm.controls['taille'].setValue(this.lastJump.taille?.toString());
this.jumpForm.controls['categorie'].setValue(this.lastJump.categorie); this.jumpForm.controls['categorie'].setValue(this.lastJump.categorie);
this.jumpForm.controls['module'].setValue(this.lastJump.module); this.jumpForm.controls['module'].setValue(this.lastJump.module);
this.jumpForm.controls['participants'].setValue(this.lastJump.participants); this.jumpForm.controls['participants'].setValue(this.lastJump.participants);
@@ -266,7 +614,7 @@ export class JumpAddDialogComponent implements OnDestroy {
this.jump.sautants = []; this.jump.sautants = [];
} }
this.jump.numero = this.nextJump; this.jump.numero = this.nextJump;
}; }
setLastAeronef(checked: boolean = false) { setLastAeronef(checked: boolean = false) {
if (checked) { if (checked) {
@@ -363,7 +711,7 @@ export class JumpAddDialogComponent implements OnDestroy {
if (checked) { if (checked) {
this.useLast.voile = true; this.useLast.voile = true;
this.jumpForm.controls['voile'].setValue(this.lastJump.voile); this.jumpForm.controls['voile'].setValue(this.lastJump.voile);
this.jumpForm.controls['taille'].setValue(this.lastJump.taille); this.jumpForm.controls['taille'].setValue(this.lastJump.taille?.toString());
} else { } else {
this.useLast.voile = false; this.useLast.voile = false;
this.jumpForm.controls['voile'].setValue(''); this.jumpForm.controls['voile'].setValue('');
@@ -377,19 +725,23 @@ export class JumpAddDialogComponent implements OnDestroy {
* Création d'un tableau de saut contenant de 1 à n saut en fonction * Création d'un tableau de saut contenant de 1 à n saut en fonction
* de la valeur du champ numero du formulaire jumpForm * de la valeur du champ numero du formulaire jumpForm
*/ */
let jumps: Array<Jump> = []; const jumps: Array<Jump> = [];
let jump: Jump = {} as Jump; const jump: Jump = {} as Jump;
let numeros: Array<number> = []; const numeros: Array<number> = [];
let date: Date; let date: Date;
/* Controle de la validité du champ date et conversion au format ISO */ /* Controle de la validité du champ date et conversion au format ISO */
let timestamp: number = Date.parse(this.jumpForm.controls['date'].value); const timestamp: number = Date.parse(this.jumpForm.controls['date'].value);
if (isNaN(timestamp) == false) { if (isNaN(timestamp) == false) {
date = new Date(timestamp); date = new Date(timestamp);
} else { } else {
date = new Date(); date = new Date();
} }
let value = this.jumpForm.controls['numero'].value;
if (typeof value !== 'string') {
value = value.toString();
}
if (isNaN(+this.jumpForm.controls['numero'].value)) { 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 * '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 * decomposer la valeur 'numero' et ajouter chaque sauts au tableau retourné par la fenêtre dialog
@@ -415,7 +767,7 @@ export class JumpAddDialogComponent implements OnDestroy {
const quantity: string[] = values[0].split('+'); const quantity: string[] = values[0].split('+');
if (quantity.length == 2) { if (quantity.length == 2) {
/* Ajout de sauts par quantité */ /* Ajout de sauts par quantité */
const start: number = jumpNum; //const start: number = jumpNum;
for (let index = 1; index <= parseInt(quantity[1]); index++) { for (let index = 1; index <= parseInt(quantity[1]); index++) {
jumpNum ++; jumpNum ++;
numeros.push(jumpNum); numeros.push(jumpNum);
@@ -434,16 +786,16 @@ export class JumpAddDialogComponent implements OnDestroy {
/* Valeurs communes aux 1 à n saut(s) à ajouter */ /* Valeurs communes aux 1 à n saut(s) à ajouter */
jump.date = date.toISOString(); jump.date = date.toISOString();
jump.lieu = this.jumpForm.controls['lieu'].value; jump.lieu = typeof this.jumpForm.controls['lieu'].value === 'string' ? this.jumpForm.controls['lieu'].value : this.jumpForm.controls['lieu'].value.lieu;
jump.oaci = this.jumpForm.controls['oaci'].value; jump.oaci = typeof this.jumpForm.controls['oaci'].value === 'string' ? this.jumpForm.controls['oaci'].value : this.jumpForm.controls['oaci'].value.oaci;
jump.aeronef = this.jumpForm.controls['aeronef'].value; jump.aeronef = typeof this.jumpForm.controls['aeronef'].value === 'string' ? this.jumpForm.controls['aeronef'].value : this.jumpForm.controls['aeronef'].value.aeronef;
jump.imat = this.jumpForm.controls['imat'].value; 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.hauteur = this.jumpForm.controls['hauteur'].value;
jump.deploiement = this.jumpForm.controls['deploiement'].value; jump.deploiement = this.jumpForm.controls['deploiement'].value;
jump.voile = this.jumpForm.controls['voile'].value;
jump.taille = this.jumpForm.controls['taille'].value;
jump.categorie = this.jumpForm.controls['categorie'].value;
jump.module = this.jumpForm.controls['module'].value;
jump.participants = this.jumpForm.controls['participants'].value; jump.participants = this.jumpForm.controls['participants'].value;
jump.sautants = this.jump.sautants; jump.sautants = this.jump.sautants;
//jump.sautants = this.jumpForm.controls['sautants'].value; //jump.sautants = this.jumpForm.controls['sautants'].value;
@@ -453,10 +805,9 @@ export class JumpAddDialogComponent implements OnDestroy {
//jump.dossier = this.jumpForm.controls['dossier'].value; //jump.dossier = this.jumpForm.controls['dossier'].value;
//jump.video = this.jumpForm.controls['video'].value; //jump.video = this.jumpForm.controls['video'].value;
//console.log(numeros);
/* Ajout du saut au tableau à retourner */ /* Ajout du saut au tableau à retourner */
numeros.forEach((numero: number) => { numeros.forEach((numero: number) => {
let item: Jump = {} as Jump; const item: Jump = {} as Jump;
Object.assign(item, jump); Object.assign(item, jump);
item.numero = numero; item.numero = numero;
item.dossier = this._computeFilepath(); item.dossier = this._computeFilepath();
@@ -1,10 +1,10 @@
<h2 mat-dialog-title class="bg-accent border-bottom border-light-subtle d-flex mb-3 pt-3 pb-2"> <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> Supprimer le saut n°{{jump.numero}}<span class="flex-spacer"></span>
<mat-icon fontIcon="close" class="pointer" (click)="closeDialog()"></mat-icon> <mat-icon fontIcon="close" class="pointer" (click)="closeDialog()"></mat-icon>
</h2> </h2>
<mat-dialog-content> <mat-dialog-content>
<div class="container"> <div class="container">
<div class="mt-2 mb-4 text-center text-danger border-warning alert alert-danger"> <div class="mt-2 mb-4 text-center text-danger border-danger alert alert-danger">
<p class="fs-6 m-0"> <p class="fs-6 m-0">
Veuillez confirmer la suppression de ce saut.<br /> 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>. <span class="fw-semibold">Attention</span>, cette <span class="fw-semibold">action</span> est <span class="fw-semibold">irréversible</span>.
@@ -7,7 +7,7 @@ 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: [ imports: [
@@ -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>
ju_solide
</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,69 @@
<h2 mat-dialog-title class="bg-cyan 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">
<h5 class="border-bottom text-light-info border-light-subtle pb-2 mb-3">Le {{ jump.date | date: 'dd/MM/yyyy' }} à {{ jump.lieu }} :</h5>
<div class="row mb-3">
<div class="col-lg-6 col-md-12">
<dl class="dl-horizontal">
<dt class="text-cyan fw-normal">Lieu:</dt>
<dd>{{jump.lieu}}</dd>
<dt class="text-cyan fw-normal">Oaci:</dt>
<dd>{{jump.oaci}}</dd>
<dt class="text-cyan fw-normal">Aeronef:</dt>
<dd>{{jump.aeronef}}</dd>
<dt class="text-cyan fw-normal">Imat:</dt>
<dd>{{jump.imat}}</dd>
<dt class="text-cyan fw-normal">Voile:</dt>
<dd>{{jump.voile}}</dd>
<dt class="text-cyan fw-normal">Taille:</dt>
<dd>{{jump.taille}} ft<sup>2</sup></dd>
<dt class="text-cyan fw-normal">Participants:</dt>
<dd>{{jump.participants}}</dd>
<dt class="text-cyan fw-normal">Sautants:</dt>
<dd>
@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>
ju_solide
</span>
</dd>
</dl>
</div>
<div class="col-lg-6 col-md-12">
<dl class="dl-horizontal">
<dt class="text-cyan fw-normal">Hauteur:</dt>
<dd>{{jump.hauteur}} m</dd>
<dt class="text-cyan fw-normal">Deploiement:</dt>
<dd>{{jump.deploiement}} m</dd>
<dt class="text-cyan fw-normal">Categorie:</dt>
<dd>{{jump.categorie}}</dd>
<dt class="text-cyan fw-normal">Module:</dt>
<dd>{{jump.module}}</dd>
<dt class="text-cyan fw-normal">Accessoires:</dt>
<dd>{{jump.accessoires}}</dd>
<dt class="text-cyan fw-normal">Zone:</dt>
<dd>{{jump.zone}}</dd>
<dt class="text-cyan fw-normal">Dossier media:</dt>
<dd>{{jump.dossier}}</dd>
<dt class="text-cyan fw-normal">Video:</dt>
<dd>{{jump.video}}</dd>
<dt class="text-cyan fw-normal">Programme:</dt>
<dd>{{jump.programme}}</dd>
</dl>
</div>
</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>
@@ -1,5 +1,5 @@
import { Component, Inject } from '@angular/core'; import { Component, Inject } from '@angular/core';
import { DatePipe, NgFor } from '@angular/common'; 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 { MatDividerModule } from '@angular/material/divider';
@@ -8,13 +8,16 @@ 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-view-dialog', selector: 'app-jump-view-dialog',
templateUrl: 'jump-view.dialog.html', templateUrl: 'jump-view.dialog.html',
standalone: true, standalone: true,
imports: [ imports: [
DatePipe, NgFor, DatePipe,
MatButtonModule, MatDialogModule, MatDividerModule, MatIconModule MatButtonModule,
] MatDialogModule,
MatDividerModule,
MatIconModule
]
}) })
export class JumpViewDialogComponent { export class JumpViewDialogComponent {
@@ -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 { }
+102 -32
View File
@@ -1,37 +1,107 @@
<div class="content"> <div @flyInOut>
<mat-card> <div class="content">
<mat-card-header class="bg-accent rounded-top py-3 px-4"> <div class="d-flex">
<mat-card-title> <div class="flex-fill">
{{ title }} <span class="fs-5" [hidden]="!lastJump.numero">({{ lastJump.numero }})</span> <h1 class="mb-1">{{title}}</h1>
</mat-card-title> <span [hidden]="!lastJump.numero" class="me-2 fs-6">
<mat-card-subtitle> 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 [hidden]="!lastJump.numero" class="me-2 fst-italic">
Dernier saut enregistré : # {{ lastJump.numero }} le {{ lastJump.date | date: 'dd/MM/yyyy' }} à {{ lastJump.lieu }}
</span> </span>
</mat-card-subtitle> </div>
<span class="flex-spacer"></span> <span class="flex-spacer"></span>
<button mat-icon-button (click)="openAddDialog()" aria-label="Ajouter"> <div class="align-self-center me-2">
<mat-icon fontIcon="add_notes"></mat-icon> <button mat-icon-button (click)="openAddDialog()" aria-label="Ajouter">
</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> <mat-icon fontIcon="add_notes"></mat-icon>
<span>Ajouter un saut</span>
</button> </button>
<ng-container *ngFor="let menuitem of menuItems.getMenuLogbook()"> <button mat-icon-button [matMenuTriggerFor]="menuJump" aria-label="Menu Jump">
<button mat-menu-item [routerLink]="['/', menuitem.state]" *ngIf="menuitem.type === 'link'"> <mat-icon fontIcon="more_vert"></mat-icon>
<mat-icon [fontIcon]="menuitem.icon"></mat-icon> </button>
<span>{{ menuitem.name }}</span> <mat-menu #menuJump="matMenu">
<button mat-menu-item (click)="openAddDialog()">
<mat-icon fontIcon="add_notes"></mat-icon>
<span>Ajouter un saut</span>
</button> </button>
</ng-container> @for (menuitem of menuItems.getMenuLogbook(); track menuitem) {
</mat-menu> @if (menuitem.type === 'link') {
</mat-card-header> <button mat-menu-item [routerLink]="['/', menuitem.state]">
<mat-card-content class="my-4"> <mat-icon [fontIcon]="menuitem.icon"></mat-icon>
<huapp-jump-table #jumpTable [config]="listConfig" [isUser]="isUser" [showAdd]="canModify" [title]="title"> <span>{{ menuitem.name }}</span>
</huapp-jump-table> </button>
</mat-card-content> }
</mat-card> }
</div> </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();
+96 -11
View File
@@ -1,17 +1,24 @@
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { Component, AfterContentChecked, OnInit, OnDestroy } from '@angular/core'; 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 { ActivatedRoute, Router, RouterModule } from '@angular/router';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card'; import { MatCardModule } from '@angular/material/card';
import { MatDividerModule } from '@angular/material/divider';
import { MatDialog, MatDialogModule } from '@angular/material/dialog'; import { MatDialog, MatDialogModule } from '@angular/material/dialog';
import { MatExpansionModule } from '@angular/material/expansion';
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 { MatMenuModule } from '@angular/material/menu';
import { MatSliderModule } from '@angular/material/slider';
import { MatSnackBar, MatSnackBarConfig, MatSnackBarHorizontalPosition, MatSnackBarModule, MatSnackBarVerticalPosition } from '@angular/material/snack-bar'; 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 { MenuItems, JumpAddDialogComponent, JumpTableComponent } from 'src/app/components/shared'; 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 { Errors, Jump, JumpListConfig, Range, User } from 'src/app/core/models';
import { JumpsService, JumpTableService, UserService } from 'src/app/core/services'; 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'; //import data from 'src/jumps_02.json';
@@ -19,20 +26,35 @@ import data from 'src/jumps.json';
@Component({ @Component({
standalone: true, standalone: true,
imports: [ imports: [
CommonModule, RouterModule, CommonModule, RouterModule, FormsModule,
MatButtonModule, MatCardModule, MatIconModule, MatButtonModule, MatCardModule, MatDividerModule, MatExpansionModule,
MatMenuModule, MatDialogModule, MatSnackBarModule, MatIconModule, MatInputModule, MatMenuModule, MatDialogModule,
MatSliderModule, MatSnackBarModule,
JumpTableComponent JumpTableComponent
], ],
selector: 'huapp-logbook', 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, AfterContentChecked { 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();
private _lastjump: Subscription = new Subscription(); private _lastjump: Subscription = new Subscription();
private _lastjump$: Observable<Jump> = new Observable(); private _lastjump$: Observable<Jump> = new Observable();
public jumpsCount = 0;
public errors!: Errors; public errors!: Errors;
public title = 'Carnet de sauts'; public title = 'Carnet de sauts';
public jump: Jump = {} as Jump; public jump: Jump = {} as Jump;
@@ -45,6 +67,21 @@ export class LogbookComponent implements OnInit, OnDestroy, AfterContentChecked
public isSubmitting = false; public isSubmitting = false;
public tableRefresh = false; public tableRefresh = false;
public jumpRefresh = 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,
@@ -59,23 +96,67 @@ export class LogbookComponent implements OnInit, OnDestroy, AfterContentChecked
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._jumpsService.getLastJump();
this._lastjump = this._lastjump$.subscribe((jump) => { this._lastjump = this._lastjump$.subscribe((jump) => {
const currentYear: number = new Date().getFullYear();
this.lastJump = jump; 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()));
}); });
*/
}); });
} }
@@ -107,7 +188,7 @@ export class LogbookComponent implements OnInit, OnDestroy, AfterContentChecked
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;
} }
}); });
@@ -134,6 +215,10 @@ export class LogbookComponent implements OnInit, OnDestroy, AfterContentChecked
}); });
} }
setTotalResults(value: number): void {
this.jumpsCount = value;
}
private _onEntry(jumps: Array<Jump>): void { private _onEntry(jumps: Array<Jump>): void {
const max: number = jumps.length; const max: number = jumps.length;
let refresh: boolean = false; let refresh: boolean = false;
@@ -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,25 @@
/* Profile */
@use 'variable' as var;
.banner {
background-color: var.$sidebar-footer;
padding: 1.6rem 0 1.3rem 0;
.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 { }
@@ -1,24 +0,0 @@
import { Injectable, } from '@angular/core';
import { ActivatedRouteSnapshot, Resolve, Router, RouterStateSnapshot } from '@angular/router';
import { Observable } from 'rxjs';
import { Question } from 'src/app/core/models';
import { QCMService } from 'src/app/core/services';
import { catchError } from 'rxjs/operators';
@Injectable()
export class QCMResolver implements Resolve<Question> {
constructor(
private _qcmService: QCMService,
private router: Router
) { }
resolve(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): Observable<any> {
return this._qcmService.get(route.params['type'])
.pipe(catchError(() => this.router.navigateByUrl('/dashboard')));
}
}
@@ -1,22 +0,0 @@
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { QCMComponent } from './qcm.component';
import { QCMResolver } from './qcm-resolver.service';
import { AuthGuard } from 'src/app/core/services';
const routes: Routes = [
{
path: ':type',
component: QCMComponent,
canActivate: [AuthGuard],
resolve: {
qcm: QCMResolver
}
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class QCMRoutingModule { }
+58 -3
View File
@@ -1,5 +1,60 @@
<div class="content"> <div class="content">
<h1>{{title}}</h1> <div class="d-flex">
<hr /> <div class="flex-fill">
<p>qcm works!</p> <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>
<mat-card [hidden]="!questions.length">
<mat-card-content class="mt-4">
<form [formGroup]="qcmForm">
@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 id="radio-group-{{ category.num }}-{{ question.num }}" class="d-flex flex-column" (change)="onChange()">
@for (choice of question.choices; track choice) {
<mat-radio-button class="example-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>
</mat-card>
</div> </div>
+121 -17
View File
@@ -1,42 +1,91 @@
import { Component, OnInit, OnDestroy } from '@angular/core'; 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 { 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 { MatRadioModule } from '@angular/material/radio';
import { Observable, Subscription } from 'rxjs'; import { Observable, Subscription } from 'rxjs';
import { Errors, QCM, User } from 'src/app/core/models'; import { MenuItems } from 'src/app/components/shared';
import { QCMService, UserService } from 'src/app/core/services'; import { ListErrorsComponent, } from 'src/app/components/shared';
import { Errors, Qcm, User } from 'src/app/core/models';
import { QcmService, UserService } from 'src/app/core/services';
import data from 'src/qcm-bpa.json';
@Component({ @Component({
selector: 'huapp-qcm', selector: 'app-qcm',
standalone: true, standalone: true,
imports: [CommonModule], imports: [
FormsModule, ReactiveFormsModule,
MatButtonModule, MatCardModule, MatDividerModule, MatExpansionModule,
MatIconModule, MatMenuModule, MatRadioModule,
ListErrorsComponent
],
templateUrl: './qcm.component.html', templateUrl: './qcm.component.html',
styleUrls: ['./qcm.component.scss'] styleUrl: './qcm.component.scss'
}) })
export class QCMComponent implements OnInit, OnDestroy { export class QcmComponent implements OnInit, OnDestroy {
private _currentUser: Subscription = new Subscription(); private _currentUser: Subscription = new Subscription();
private _data: Subscription = new Subscription(); private _data: Subscription = new Subscription();
public qcm: QCM = {} as QCM; public qcm: Qcm = {} as Qcm;
public currentUser: User = {} as User; public currentUser: User = {} as User;
public canModify = false; public canModify = false;
public title = 'QCM Brevets'; 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 qcmForm!: FormGroup;
constructor( constructor(
private route: ActivatedRoute, private route: ActivatedRoute,
private _qcmService: QCMService, private fb: FormBuilder,
private _userService: UserService private _qcmService: QcmService,
) { } private _userService: UserService,
public menuItems: MenuItems
) {
this.qcmForm = this.fb.group({
questions: this.fb.array([])
});
}
get questions(): FormArray {
return <FormArray>this.qcmForm.controls['questions'];
}
ngOnInit() { ngOnInit() {
// Retreive the prefetched jump //this.importQuestions();
const data$: Observable<any> = this.route.data; //this.importChoices();
this._data = data$.subscribe((data: { qcm: QCM }) => { // 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.qcm = data.qcm; this.qcm = data.qcm;
if (this.qcm.name == 'bpa') { if (this.qcm.name === 'bpa') {
this.title = 'QCM du BPA'; this.title = 'QCM du BPA';
this.subtitle = 'Préparation au questionnaire du BPA';
} else { } else {
this.title = `QCM Brevets ${this.qcm.name.toUpperCase()}`; 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.questions.push(
this.fb.group({
id: new FormControl({ value: question.num, disabled: false }),
choice: ['']
//choice: new FormControl({ value: '', disabled: false })
})
);
});
});
}); });
// Load the current user's data // Load the current user's data
const currentUser$: Observable<User> = this._userService.currentUser; const currentUser$: Observable<User> = this._userService.currentUser;
@@ -50,4 +99,59 @@ export class QCMComponent implements OnInit, OnDestroy {
this._data.unsubscribe(); this._data.unsubscribe();
this._currentUser.unsubscribe(); this._currentUser.unsubscribe();
} }
setExpandedIndex(index: number) {
this.expandedIndex = index;
}
onChange() {
//this.isDisabled = true;
}
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);
}
}
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);
}
}
} }
-19
View File
@@ -1,19 +0,0 @@
import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { QCMComponent } from './qcm.component';
import { QCMResolver } from './qcm-resolver.service';
import { QCMRoutingModule } from './qcm-routing.module';
@NgModule({
imports: [
QCMRoutingModule,
QCMComponent
],
providers: [
QCMResolver
],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class QCMModule { }
@@ -0,0 +1,45 @@
<div class="banner bg-navy light text-center mat-elevation-z2">
<div class="d-flex justify-content-center text-start">
<img [src]="user.image" class="user-img" [alt]="user.username" />
<div class="ms-3">
<h4>{{ bannerTitle }}</h4>
<button mat-raised-button color="cyan" [routerLink]="['/profile', user.username]">
<mat-icon aria-label="Voir le compte" fontIcon="person"></mat-icon> Mon compte
</button>
<button mat-raised-button color="warn" routerLink="/settings" class="ms-2">
<mat-icon aria-label="Modifier le compte" fontIcon="settings"></mat-icon> Paramètres
</button>
</div>
</div>
</div>
<div class="credentials-page content">
<h1>{{title}}</h1>
<mat-divider></mat-divider>
<app-list-errors [errors]="errors"></app-list-errors>
<div class="container page mt-4">
<div class="row">
<div class="col-md-6 offset-md-3 col-xs-12">
<form class="row row-cols-lg-auto" [formGroup]="credentialsForm" (ngSubmit)="submitForm()">
<fieldset [disabled]="isSubmitting" class="w-100">
<legend [hidden]="true">Mise à jour du mot de passe</legend>
<div class="col-12">
<mat-form-field appearance="fill" class="w-100">
<mat-label>Mot de passe</mat-label>
<input matInput type="password" placeholder="Nouveau mot de passe" formControlName="password">
</mat-form-field>
</div>
<div class="col-12">
<mat-form-field appearance="fill" class="w-100">
<mat-label>Confirmation du mot de passe</mat-label>
<input matInput type="password" placeholder="Confirmez votre nouveau mot de passe" formControlName="confirmPassword">
</mat-form-field>
</div>
<div class="col-12">
<button mat-raised-button color="primary" [disabled]="!credentialsForm.valid" type="submit" class="float-sm-end">{{ btnUpdateTitle }}</button>
</div>
</fieldset>
</form>
</div>
</div>
</div>
</div>
@@ -0,0 +1,34 @@
/* Credentials */
@use 'variable' as var;
.banner {
background-color: var.$sidebar-footer;
padding: 1.6rem 0 1.3rem 0;
.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;
}
}
/* Chrome, Safari, Edge, Opera */
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
appearance: none;
-webkit-appearance: none;
margin: 0;
}
/* Firefox */
input[type=number] {
appearance: textfield;
-moz-appearance: textfield;
}
@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { CredentialsComponent } from './credentials.component';
describe('CredentialsComponent', () => {
let component: CredentialsComponent;
let fixture: ComponentFixture<CredentialsComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [CredentialsComponent]
})
.compileComponents();
fixture = TestBed.createComponent(CredentialsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,95 @@
import { Component, DestroyRef, inject, OnInit, OnDestroy } from '@angular/core';
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { AbstractControl, FormsModule, ReactiveFormsModule, UntypedFormBuilder, UntypedFormGroup, Validators, ValidatorFn, ValidationErrors } from '@angular/forms';
import { Router, RouterLink } from '@angular/router';
import { Title } from '@angular/platform-browser';
import { MatButtonModule } from '@angular/material/button';
import { MatDividerModule } from '@angular/material/divider';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { Observable, 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({
selector: 'app-credentials',
standalone: true,
imports: [
RouterLink, FormsModule, ReactiveFormsModule,
MatButtonModule, MatIconModule, MatFormFieldModule, MatInputModule, MatDividerModule,
ListErrorsComponent
],
templateUrl: './credentials.component.html',
styleUrl: './credentials.component.scss'
})
export class CredentialsComponent implements OnInit, OnDestroy {
private _user: Subscription = new Subscription();
title = 'Modifier le mot de passe';
bannerTitle = '';
btnUpdateTitle = 'Mettre à jour';
btnLogoutTitle = 'Se déconnecter';
user: Partial<User> = {} as Partial<User>;
credentialsForm: UntypedFormGroup;
errors: Errors = { errors: {} };
isSubmitting = false;
destroyRef = inject(DestroyRef);
constructor(
private router: Router,
private titleService: Title,
private userService: UserService,
private fb: UntypedFormBuilder
) {
this.credentialsForm = this.fb.group({
password: ['', Validators.required],
confirmPassword: ['', Validators.required]
}, { validators: this.checkPasswords });
}
get f() {
return this.credentialsForm.controls;
}
ngOnInit() {
this.titleService.setTitle(`Head Up - ${this.title}`);
Object.assign(this.user, this.userService.getCurrentUser());
this.credentialsForm.patchValue(this.user);
if (this.user.username) {
this.bannerTitle = `@${this.user.username}`;
} else {
this.bannerTitle = `${this.user.firstname} ${this.user.lastname}`;
}
}
ngOnDestroy() {
this._user.unsubscribe();
}
submitForm() {
this.isSubmitting = true;
this.updateUser(this.credentialsForm.value);
const user$: Observable<{ user: User }> = this.userService.update(this.user).pipe(takeUntilDestroyed(this.destroyRef));
this._user = user$.subscribe({
next: ({ user }) => void this.router.navigateByUrl('/profile/' + user.username),
error: (err) => {
this.errors = err;
this.isSubmitting = false;
}
});
}
updateUser(values: NonNullable<unknown>) {
Object.assign(this.user, values);
}
checkPasswords: ValidatorFn = (group: AbstractControl): ValidationErrors | null => {
const pass = group.get('password')!.value;
const confirmPass = group.get('confirmPassword')!.value
return pass === confirmPass ? null : { notSame: true }
}
}
+2
View File
@@ -0,0 +1,2 @@
export * from './credentials/credentials.component';
export * from './settings.component';
@@ -1,18 +0,0 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { SettingsComponent } from './settings.component';
import { AuthGuard } from 'src/app/core/services';
const routes: Routes = [
{
path: '',
component: SettingsComponent,
canActivate: [AuthGuard]
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class SettingsRoutingModule { }
@@ -1,22 +1,24 @@
<div class="settings-page"> <div class="banner bg-navy light text-center mat-elevation-z2">
<div class="banner light text-center pt-4 pb-3 mat-elevation-z2"> <div class="d-flex justify-content-center text-start">
<div class="container"> <img [src]="user.image" class="user-img" [alt]="user.username" />
<div class="row"> <div class="ms-3">
<div class="col-xs-12"> <h4>{{ bannerTitle }}</h4>
<img [src]="user.image" class="user-img" [alt]="user.username" /> <button mat-raised-button color="cyan" [routerLink]="['/profile', user.username]">
<h4>{{ bannerTitle }}</h4> <mat-icon aria-label="Voir le compte" fontIcon="person"></mat-icon> Mon compte
<button mat-stroked-button color="accent" [routerLink]="['/profile', user.username]"> </button>
<mat-icon aria-label="Voir le compte" fontIcon="person"></mat-icon> Mon compte <button mat-raised-button color="warn" [routerLink]="['/settings', 'credentials']" class="ms-2">
</button> <mat-icon aria-label="Modifier le compte" fontIcon="password"></mat-icon> {{ btnPasswordTitle }}
</div> </button>
</div>
</div> </div>
</div> </div>
<div class="container page"> </div>
<div class="settings-page content">
<h1>{{title}}</h1>
<mat-divider></mat-divider>
<app-list-errors [errors]="errors"></app-list-errors>
<div class="container page mt-4">
<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 mb-3">{{ title }}</h1>
<huapp-list-errors [errors]="errors"></huapp-list-errors>
<form class="row row-cols-lg-auto" [formGroup]="settingsForm" (ngSubmit)="submitForm()"> <form class="row row-cols-lg-auto" [formGroup]="settingsForm" (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>
@@ -77,25 +79,12 @@
<input matInput readonly type="email" placeholder="Indiquez votre adresse email" formControlName="email"> <input matInput readonly type="email" placeholder="Indiquez votre adresse email" formControlName="email">
</mat-form-field> </mat-form-field>
</div> </div>
<div class="col-12"> <div class="col-12 clearfix">
<mat-form-field appearance="fill" class="w-100">
<mat-label>Mot de passe</mat-label>
<input matInput type="password" placeholder="Nouveau mot de passe" formControlName="password">
</mat-form-field>
</div>
<div class="col-12">
<mat-form-field appearance="fill" class="w-100">
<mat-label>Confirmation du mot de passe</mat-label>
<input matInput type="password" placeholder="Confirmez votre nouveau mot de passe" formControlName="confirmPassword">
</mat-form-field>
</div>
<div class="col-12">
<button mat-raised-button color="primary" [disabled]="!settingsForm.valid" type="submit" class="float-sm-end">{{ btnUpdateTitle }}</button> <button mat-raised-button color="primary" [disabled]="!settingsForm.valid" type="submit" class="float-sm-end">{{ btnUpdateTitle }}</button>
<button mat-stroked-button color="warn" class="me-3" type="button" (click)="logout()">{{ btnLogoutTitle }}</button>
</div> </div>
</fieldset> </fieldset>
</form> </form>
<mat-divider class="my-3 border-light-subtle"></mat-divider>
<button mat-stroked-button color="warn" (click)="logout()">{{ btnLogoutTitle }}</button>
</div> </div>
</div> </div>
</div> </div>
@@ -1,9 +1,10 @@
/* Settings */ /* Settings */
@use '@angular/material' as mat; @use 'variable' as var;
@use 'src/styles/variable' as var;
//@import 'src/styles/variable';
.banner { .banner {
background-color: var.$sidebar-footer;
padding: 1.6rem 0 1.3rem 0;
/*
background-color: mat.get-color-from-palette(var.$md-custom-warn, 700); background-color: mat.get-color-from-palette(var.$md-custom-warn, 700);
margin-bottom: 1rem; margin-bottom: 1rem;
&.light { &.light {
@@ -14,11 +15,11 @@
border-color: #555; border-color: #555;
} }
} }
*/
.user-img { .user-img {
width: 84px; width: var.$user-img-size;
height: 84px; height: var.$user-img-size;
border-radius: 84px; border-radius: var.$user-img-size;
margin-bottom: 0.75rem;
} }
h4 { h4 {
font-weight: 500; font-weight: 500;
@@ -26,7 +27,7 @@
} }
p { p {
margin: 0 auto .5rem; margin: 0 auto .5rem;
color: #aaaaaa; color: var.$muted;
max-width: 450px; max-width: 450px;
font-weight: 300; font-weight: 300;
} }
@@ -38,7 +39,6 @@ input::-webkit-inner-spin-button {
-webkit-appearance: none; -webkit-appearance: none;
margin: 0; margin: 0;
} }
/* Firefox */ /* Firefox */
input[type=number] { input[type=number] {
appearance: textfield; appearance: textfield;
@@ -1,5 +1,6 @@
import { Component, OnInit, OnDestroy } from '@angular/core'; import { Component, DestroyRef, inject, OnInit, OnDestroy } from '@angular/core';
import { AbstractControl, FormsModule, ReactiveFormsModule, UntypedFormBuilder, UntypedFormGroup, Validators, ValidatorFn, ValidationErrors } from '@angular/forms'; import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { FormsModule, ReactiveFormsModule, UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
import { Router, RouterLink } from '@angular/router'; import { Router, RouterLink } from '@angular/router';
import { Title } from '@angular/platform-browser'; import { Title } from '@angular/platform-browser';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
@@ -9,22 +10,21 @@ import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input'; import { MatInputModule } from '@angular/material/input';
import { Observable, Subscription } from 'rxjs'; import { Observable, Subscription } from 'rxjs';
import { take } from 'rxjs/operators';
import { Errors, User } from 'src/app/core/models'; import { Errors, User } from 'src/app/core/models';
import { UserService } from 'src/app/core/services'; import { UserService } from 'src/app/core/services';
import { ListErrorsComponent } from 'src/app/components/shared'; import { ListErrorsComponent } from 'src/app/components/shared';
@Component({ @Component({
selector: 'huapp-settings-page', selector: 'app-settings-page',
styleUrls: ['settings.component.scss'],
templateUrl: './settings.component.html',
standalone: true, standalone: true,
imports: [ imports: [
RouterLink, FormsModule, ReactiveFormsModule, RouterLink, FormsModule, ReactiveFormsModule,
MatButtonModule, MatIconModule, MatFormFieldModule, MatInputModule, MatDividerModule, MatButtonModule, MatIconModule, MatFormFieldModule, MatInputModule, MatDividerModule,
ListErrorsComponent ListErrorsComponent
] ],
styleUrl: './settings.component.scss',
templateUrl: './settings.component.html'
}) })
export class SettingsComponent implements OnInit, OnDestroy { export class SettingsComponent implements OnInit, OnDestroy {
private _user: Subscription = new Subscription(); private _user: Subscription = new Subscription();
@@ -32,10 +32,12 @@ export class SettingsComponent implements OnInit, OnDestroy {
bannerTitle = ''; bannerTitle = '';
btnUpdateTitle = 'Mettre à jour'; btnUpdateTitle = 'Mettre à jour';
btnLogoutTitle = 'Se déconnecter'; btnLogoutTitle = 'Se déconnecter';
user: User = {} as User; btnPasswordTitle = 'Changer le mot de passe';
user: Partial<User> = {} as Partial<User>;
settingsForm: UntypedFormGroup; settingsForm: UntypedFormGroup;
errors: Errors = { errors: {} }; errors: Errors = { errors: {} };
isSubmitting = false; isSubmitting = false;
destroyRef = inject(DestroyRef);
constructor( constructor(
private router: Router, private router: Router,
@@ -50,13 +52,11 @@ export class SettingsComponent implements OnInit, OnDestroy {
firstname: ['', Validators.required], firstname: ['', Validators.required],
lastname: ['', Validators.required], lastname: ['', Validators.required],
phone: ['', Validators.required], phone: ['', Validators.required],
password: ['', Validators.required],
confirmPassword: ['', Validators.required],
licence: '', licence: '',
poids: '', poids: '',
image: '', image: '',
bg_image: '' bg_image: ''
}, { validators: this.checkPasswords }); });
// Optional: subscribe to changes on the form : // Optional: subscribe to changes on the form :
// this.settingsForm.valueChanges.pipe(this._scavenger.collect()).subscribe(values => this.updateUser(values)); // this.settingsForm.valueChanges.pipe(this._scavenger.collect()).subscribe(values => this.updateUser(values));
} }
@@ -87,15 +87,19 @@ export class SettingsComponent implements OnInit, OnDestroy {
this.router.navigateByUrl('/login'); this.router.navigateByUrl('/login');
} }
updatePass() {
this.router.navigateByUrl('/settings/credentials');
}
submitForm() { submitForm() {
this.isSubmitting = true; this.isSubmitting = true;
// update the model // update the model
this.updateUser(this.settingsForm.value); this.updateUser(this.settingsForm.value);
const user$: Observable<User> = this.userService.update(this.user).pipe(take(1)); const user$: Observable<{ user: User }> = this.userService.update(this.user).pipe(takeUntilDestroyed(this.destroyRef));
this._user = user$.subscribe({ this._user = user$.subscribe({
next: (updatedUser) => this.router.navigateByUrl('/profile/' + updatedUser.username), next: ({ user }) => void this.router.navigateByUrl('/profile/' + user.username),
error: (err) => { error: (err) => {
this.errors = err; this.errors = err;
this.isSubmitting = false; this.isSubmitting = false;
@@ -107,10 +111,4 @@ export class SettingsComponent implements OnInit, OnDestroy {
Object.assign(this.user, values); Object.assign(this.user, values);
} }
checkPasswords: ValidatorFn = (group: AbstractControl): ValidationErrors | null => {
const pass = group.get('password')!.value;
const confirmPass = group.get('confirmPassword')!.value
return pass === confirmPass ? null : { notSame: true }
}
} }
@@ -1,12 +0,0 @@
import { NgModule } from '@angular/core';
import { SettingsComponent } from './settings.component';
import { SettingsRoutingModule } from './settings-routing.module';
@NgModule({
imports: [
SettingsRoutingModule,
SettingsComponent
]
})
export class SettingsModule { }
@@ -1,46 +1,42 @@
<mat-card> <mat-card>
<mat-card-header> <mat-card-header>
<mat-card-title>{{ title }}</mat-card-title> <mat-card-title>{{ title }}</mat-card-title>
<mat-card-subtitle>{{ subtitle }}</mat-card-subtitle> <mat-card-subtitle>{{ subtitle }}</mat-card-subtitle>
<span class="flex-spacer"></span> <span class="flex-spacer"></span>
<button mat-icon-button [matMenuTriggerFor]="menuAeronefBar" aria-label="Menu Aeronef Bar"> <button mat-icon-button [matMenuTriggerFor]="menuAeronefBar" aria-label="Menu Aeronef Bar">
<mat-icon fontIcon="more_vert"></mat-icon> <mat-icon fontIcon="more_vert"></mat-icon>
</button> </button>
<mat-menu #menuAeronefBar="matMenu"> <mat-menu #menuAeronefBar="matMenu">
<button mat-menu-item *ngFor="let menuitem of menuItems.getMenuPanel()"> @for (menuitem of menuItems.getMenuPanel(); track menuitem) {
<mat-icon [fontIcon]="menuitem.icon"></mat-icon> <button mat-menu-item>
<span>{{ menuitem.name }}</span> <mat-icon [fontIcon]="menuitem.icon"></mat-icon>
</button> <span>{{ menuitem.name }}</span>
</mat-menu> </button>
</mat-card-header> }
<hr> </mat-menu>
<mat-card-content> </mat-card-header>
<div *ngIf="displayCharts" class="barchart position-relative w-100 my-1"> <mat-divider class="my-3"></mat-divider>
<canvas baseChart <mat-card-content>
[data]="chartConfig.barChartData" @if (displayCharts) {
[options]="chartConfig.barChartOptions" <app-bar-horizontal-chart [headers]="seriesHeader" [names]="seriesName" [values]="seriesValue" [colors]="seriesColor" label="Nombre total de sauts" [legend]="false"></app-bar-horizontal-chart>
[plugins]="chartConfig.barChartPlugins" }
[legend]="chartConfig.barChartLegend" </mat-card-content>
[type]="'bar'"> <mat-divider class="mt-3 mb-0"></mat-divider>
</canvas> <mat-accordion>
</div> <mat-expansion-panel>
</mat-card-content> <mat-expansion-panel-header>
<hr class="mb-0"> <mat-panel-title>Historique annuel</mat-panel-title>
<mat-accordion> </mat-expansion-panel-header>
<mat-expansion-panel> <app-history-table [headers]="seriesHeader" [names]="seriesName" [values]="seriesValue" [rows]="seriesRow" [colors]="seriesColorClass"></app-history-table>
<mat-expansion-panel-header> <!--
<mat-panel-title>Historique annuel</mat-panel-title> <div fxLayout="row wrap">
</mat-expansion-panel-header> <div *ngFor='let name of seriesName; let i=index' fxFlex.gt-lg="33" fxFlex.gt-md="50" fxFlex.gt-xs="20" fxFlex="100">
<huapp-history-table [headers]="seriesHeader" [names]="seriesName" [values]="seriesValue" [rows]="seriesRow" [colors]="seriesColor"></huapp-history-table> <h6 class="{{seriesColor[i]}} m-0">
<!-- <i class="mdi mdi-checkbox-blank-circle font-10 m-r-10 "></i>{{name}} - {{seriesValue[i]}}
<div fxLayout="row wrap"> </h6>
<div *ngFor='let name of seriesName; let i=index' fxFlex.gt-lg="33" fxFlex.gt-md="50" fxFlex.gt-xs="20" fxFlex="100"> </div>
<h6 class="{{seriesColor[i]}} m-0"> </div>
<i class="mdi mdi-checkbox-blank-circle font-10 m-r-10 "></i>{{name}} - {{seriesValue[i]}} -->
</h6> </mat-expansion-panel>
</div> </mat-accordion>
</div> </mat-card>
-->
</mat-expansion-panel>
</mat-accordion>
</mat-card>
@@ -7,9 +7,9 @@ describe('AeronefsBarComponent', () => {
let fixture: ComponentFixture<AeronefsBarComponent>; let fixture: ComponentFixture<AeronefsBarComponent>;
beforeEach(waitForAsync(() => { beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [ AeronefsBarComponent ] imports: [AeronefsBarComponent]
}) })
.compileComponents(); .compileComponents();
})); }));
@@ -1,27 +1,27 @@
import { Component, OnInit, OnDestroy } from '@angular/core'; import { Component, DestroyRef, inject, OnInit, OnDestroy } from '@angular/core';
import { CommonModule } from '@angular/common'; import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card'; import { MatCardModule } from '@angular/material/card';
import { MatDividerModule } from '@angular/material/divider';
import { MatExpansionModule } from '@angular/material/expansion'; import { MatExpansionModule } from '@angular/material/expansion';
import { MatIconModule } from '@angular/material/icon'; import { MatIconModule } from '@angular/material/icon';
import { MatMenuModule } from '@angular/material/menu'; import { MatMenuModule } from '@angular/material/menu';
import { ChartDataset } from 'chart.js';
import { NgChartsModule } from 'ng2-charts';
import { Observable, Subscription } from 'rxjs'; import { Observable, Subscription } from 'rxjs';
import { MenuItems, HistoryTableComponent } from 'src/app/components/shared'; import { MenuItems } from 'src/app/components/shared';
import { BarHorizontalChartComponent } from 'src/app/components/shared/helpers-chart';
import { HistoryTableComponent } from 'src/app/components/shared/helpers-jump';
import { UtilitiesService, AeronefsService } from 'src/app/core/services'; import { UtilitiesService, AeronefsService } from 'src/app/core/services';
import { AeronefByImat, AeronefByYear, BarConfig } from 'src/app/core/models'; import { AeronefByImat, AeronefByYear } from 'src/app/core/models';
@Component({ @Component({
selector: 'app-aeronefs-bar',
standalone: true, standalone: true,
imports: [ imports: [
CommonModule, MatButtonModule, MatCardModule, MatDividerModule,
MatButtonModule, MatCardModule, MatExpansionModule, MatIconModule, MatMenuModule, MatExpansionModule, MatIconModule, MatMenuModule,
NgChartsModule, BarHorizontalChartComponent, HistoryTableComponent
HistoryTableComponent
], ],
selector: 'huapp-aeronefs-bar',
templateUrl: './aeronefs-bar.component.html' templateUrl: './aeronefs-bar.component.html'
}) })
export class AeronefsBarComponent implements OnInit, OnDestroy { export class AeronefsBarComponent implements OnInit, OnDestroy {
@@ -33,11 +33,18 @@ export class AeronefsBarComponent implements OnInit, OnDestroy {
public subtitle: string = 'Nombre total de sauts par aéronef'; public subtitle: string = 'Nombre total de sauts par aéronef';
public seriesHeader: string[] = []; public seriesHeader: string[] = [];
public seriesName: string[] = []; public seriesName: string[] = [];
public seriesValue: any[] = []; public seriesValue: number[] = [];
public seriesRow: any[] = []; public seriesRow: Array<Array<number>> = [];
public seriesColor: string[] = this._utilitiesService.getChartColors(); 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 displayCharts = false; public displayCharts = false;
public chartConfig: BarConfig = this._utilitiesService.getHorizontalBarChartConfig(); public destroyRef = inject(DestroyRef);
constructor( constructor(
private _aeronefsService: AeronefsService, private _aeronefsService: AeronefsService,
@@ -55,44 +62,34 @@ export class AeronefsBarComponent implements OnInit, OnDestroy {
} }
private _loadAeronefByImat(): void { private _loadAeronefByImat(): void {
let values: Array<number> = []; const aeronefs$: Observable<Array<AeronefByImat>> = this._aeronefsService.getAllByImat().pipe(takeUntilDestroyed(this.destroyRef));
const aeronefs$: Observable<Array<AeronefByImat>> = this._aeronefsService.getAllByImat();
this._aeronefByImat = aeronefs$.subscribe((aggregate: Array<AeronefByImat>) => { this._aeronefByImat = aeronefs$.subscribe((aggregate: Array<AeronefByImat>) => {
this._aeronefsByImat = aggregate.map((row: AeronefByImat) => { this._aeronefsByImat = aggregate.map((row: AeronefByImat) => {
//this.chartConfig.barChartData.labels!.push(`${row._id.aeronef} ${row._id.imat}`); //this.chartConfig.barChartData.labels!.push(`${row.aeronef} ${row.imat}`);
this.seriesName.push(`${row._id.aeronef} ${row._id.imat}`); this.seriesName.push(`${row.aeronef} ${row.imat}`);
this.seriesValue.push(row.count); this.seriesValue.push(row.count);
return row; return row;
}); });
let data: ChartDataset<'bar'> = {
data: [...this.seriesValue],
label: 'Nombre total de sauts',
backgroundColor: this._utilitiesService.getSeriesColors(0.9),
borderColor: 'rgba(255, 255, 255, 1)', //this._utilitiesService.getSeriesColors(1),
borderWidth: 0
};
this.chartConfig.barChartData.labels = [...this.seriesName];
this.chartConfig.barChartData.datasets!.push(data);
this.displayCharts = true;
this._loadAeronefByYear(); this._loadAeronefByYear();
}); });
} }
private _loadAeronefByYear(): void { private _loadAeronefByYear(): void {
this.seriesHeader = ["2018", "2019", "2020", "2021", "2022", "2023"]; this.seriesHeader = ['2018', '2019', '2020', '2021', '2022', '2023', '2024'];
let values: Array<number> = this.seriesHeader.map(() => 0); const values: Array<number> = this.seriesHeader.map(() => 0);
this.seriesName.forEach(() => { this.seriesName.forEach(() => {
this.seriesRow.push([...values]); this.seriesRow.push([...values]);
}); });
const dropzones$: Observable<Array<AeronefByYear>> = this._aeronefsService.getAllByDate(); const dropzones$: Observable<Array<AeronefByYear>> = this._aeronefsService.getAllByImatByYear().pipe(takeUntilDestroyed(this.destroyRef));
this._aeronefByYear = dropzones$.subscribe((aggregate: Array<AeronefByYear>) => { this._aeronefByYear = dropzones$.subscribe((aggregate: Array<AeronefByYear>) => {
this._aeronefsByYear = aggregate.map((row: AeronefByYear) => { this._aeronefsByYear = aggregate.map((row: AeronefByYear) => {
let aeronef: string = row._id.aeronef; const aeronef: string = row.aeronef;
let imat: string = row._id.imat; const imat: string = row.imat;
let year: string = row._id.year.toString(); const year: string = row.year.toString();
this.seriesRow[this.seriesName.indexOf(`${aeronef} ${imat}`)][this.seriesHeader.indexOf(year)] = row.count; this.seriesRow[this.seriesName.indexOf(`${aeronef} ${imat}`)][this.seriesHeader.indexOf(year)] = row.count;
return row; return row;
}); });
this.displayCharts = true;
}); });
} }
} }
@@ -1,47 +1,42 @@
<mat-card> <mat-card>
<mat-card-header> <mat-card-header>
<mat-card-title>{{ title }}</mat-card-title> <mat-card-title>{{ title }}</mat-card-title>
<mat-card-subtitle>{{ subtitle }}</mat-card-subtitle> <mat-card-subtitle>{{ subtitle }}</mat-card-subtitle>
<span class="flex-spacer"></span> <span class="flex-spacer"></span>
<button mat-icon-button [matMenuTriggerFor]="menuAeronef" aria-label="Menu Aeronef"> <button mat-icon-button [matMenuTriggerFor]="menuAeronef" aria-label="Menu Aeronef">
<mat-icon fontIcon="more_vert"></mat-icon> <mat-icon fontIcon="more_vert"></mat-icon>
</button> </button>
<mat-menu #menuAeronef="matMenu"> <mat-menu #menuAeronef="matMenu">
<button mat-menu-item *ngFor="let menuitem of menuItems.getMenuPanel()"> @for (menuitem of menuItems.getMenuPanel(); track menuitem) {
<mat-icon [fontIcon]="menuitem.icon"></mat-icon> <button mat-menu-item>
<span>{{ menuitem.name }}</span> <mat-icon [fontIcon]="menuitem.icon"></mat-icon>
</button> <span>{{ menuitem.name }}</span>
</mat-menu> </button>
</mat-card-header> }
<hr> </mat-menu>
<mat-card-content> </mat-card-header>
<div *ngIf="displayCharts" class="piechart position-relative w-100 my-1"> <mat-divider class="my-3"></mat-divider>
<canvas baseChart class="mx-auto" <mat-card-content>
[labels]="chartConfig.doughnutChartLabels" <app-pie-chart [headers]="seriesHeader" [names]="seriesName" [values]="seriesValue" [colors]="seriesColor"></app-pie-chart>
[datasets]="chartConfig.doughnutChartDatasets" </mat-card-content>
[options]="chartConfig.doughnutChartOptions" <mat-divider class="mt-3 mb-0"></mat-divider>
[legend]="chartConfig.doughnutChartLegend" <mat-accordion>
[type]="'doughnut'"> <mat-expansion-panel>
</canvas> <mat-expansion-panel-header>
</div> <mat-panel-title>Historique annuel</mat-panel-title>
</mat-card-content> </mat-expansion-panel-header>
<hr class="mb-0"> <app-history-table [headers]="seriesHeader" [names]="seriesName" [values]="seriesValue" [rows]="seriesRow" [colors]="seriesColorClass"></app-history-table>
<mat-accordion> </mat-expansion-panel>
<mat-expansion-panel> </mat-accordion>
<mat-expansion-panel-header> <!--
<mat-panel-title>Historique annuel</mat-panel-title> <mat-divider class="my-3"></mat-divider>
</mat-expansion-panel-header> <mat-card-content>
<huapp-history-table [headers]="seriesHeader" [names]="seriesName" [values]="seriesValue" [rows]="seriesRow" [colors]="seriesColor"></huapp-history-table> <div fxLayout="row wrap">
</mat-expansion-panel> <div *ngFor='let name of seriesName; let i=index' fxFlex.gt-lg="33" fxFlex.gt-md="50" fxFlex.gt-xs="20" fxFlex="100">
</mat-accordion> <h6 class="{{seriesColor[i]}} m-0">
<!--<hr> <i class="mdi mdi-checkbox-blank-circle font-10 m-r-10 "></i>{{name}} - {{seriesValue[i]}}
<mat-card-content> </h6>
<div fxLayout="row wrap"> </div>
<div *ngFor='let name of seriesName; let i=index' fxFlex.gt-lg="33" fxFlex.gt-md="50" fxFlex.gt-xs="20" fxFlex="100"> </div>
<h6 class="{{seriesColor[i]}} m-0"> </mat-card-content>-->
<i class="mdi mdi-checkbox-blank-circle font-10 m-r-10 "></i>{{name}} - {{seriesValue[i]}} </mat-card>
</h6>
</div>
</div>
</mat-card-content>-->
</mat-card>
@@ -7,9 +7,9 @@ describe('AeronefsComponent', () => {
let fixture: ComponentFixture<AeronefsComponent>; let fixture: ComponentFixture<AeronefsComponent>;
beforeEach(waitForAsync(() => { beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [ AeronefsComponent ] imports: [AeronefsComponent]
}) })
.compileComponents(); .compileComponents();
})); }));
@@ -1,28 +1,28 @@
import { Component, OnInit, OnDestroy } from '@angular/core'; import { Component, DestroyRef, inject, OnInit, OnDestroy } from '@angular/core';
import { CommonModule } from '@angular/common'; import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { Router, RouterLink, RouterModule } from '@angular/router';
import { MatBadgeModule } from '@angular/material/badge'; import { MatBadgeModule } from '@angular/material/badge';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card'; import { MatCardModule } from '@angular/material/card';
import { MatDividerModule } from '@angular/material/divider';
import { MatExpansionModule } from '@angular/material/expansion'; import { MatExpansionModule } from '@angular/material/expansion';
import { MatIconModule } from '@angular/material/icon'; import { MatIconModule } from '@angular/material/icon';
import { MatMenuModule } from '@angular/material/menu'; import { MatMenuModule } from '@angular/material/menu';
import { NgChartsModule } from 'ng2-charts';
import { Observable, Subscription } from 'rxjs'; import { Observable, Subscription } from 'rxjs';
import { MenuItems, HistoryTableComponent } from 'src/app/components/shared'; import { MenuItems } from 'src/app/components/shared';
import { HistoryTableComponent } from 'src/app/components/shared/helpers-jump';
import { PieChartComponent } from 'src/app/components/shared/helpers-chart';
import { UtilitiesService, AeronefsService } from 'src/app/core/services'; import { UtilitiesService, AeronefsService } from 'src/app/core/services';
import { AeronefByImat, AeronefByYear, DoughnutConfig } from 'src/app/core/models'; import { AeronefByImat, AeronefByYear } from 'src/app/core/models';
@Component({ @Component({
selector: 'app-aeronefs',
standalone: true, standalone: true,
imports: [ imports: [
CommonModule, RouterModule, MatBadgeModule, MatButtonModule, MatCardModule, MatDividerModule,
MatBadgeModule, MatButtonModule, MatCardModule, MatExpansionModule, MatIconModule, MatMenuModule, MatExpansionModule, MatIconModule, MatMenuModule,
NgChartsModule, HistoryTableComponent, PieChartComponent
HistoryTableComponent
], ],
selector: 'huapp-aeronefs',
templateUrl: './aeronefs.component.html' templateUrl: './aeronefs.component.html'
}) })
export class AeronefsComponent implements OnInit, OnDestroy { export class AeronefsComponent implements OnInit, OnDestroy {
@@ -35,13 +35,18 @@ export class AeronefsComponent implements OnInit, OnDestroy {
public seriesHeader: string[] = []; public seriesHeader: string[] = [];
public seriesName: string[] = []; public seriesName: string[] = [];
public seriesValue: number[] = []; public seriesValue: number[] = [];
public seriesRow: any[] = []; public seriesRow: Array<Array<number>> = [];
public seriesColor: string[] = this._utilitiesService.getChartColors(); public seriesColor: {
public displayCharts = false; backgroundColor: string[],
public chartConfig: DoughnutConfig = this._utilitiesService.getDoughnutChartConfig(); borderColor: string[]
} = {
backgroundColor: this._utilitiesService.getSeriesColors(0.8, 'all'),
borderColor: this._utilitiesService.getSeriesColors(1, 'all')
};
public seriesColorClass: string[] = this._utilitiesService.getChartColors();
public destroyRef = inject(DestroyRef);
constructor( constructor(
private router: Router,
private _aeronefsService: AeronefsService, private _aeronefsService: AeronefsService,
private _utilitiesService: UtilitiesService, private _utilitiesService: UtilitiesService,
public menuItems: MenuItems public menuItems: MenuItems
@@ -57,32 +62,29 @@ export class AeronefsComponent implements OnInit, OnDestroy {
} }
private _loadAeronefByImat(): void { private _loadAeronefByImat(): void {
const aeronefs$: Observable<Array<AeronefByImat>> = this._aeronefsService.getAllByImat(); const aeronefs$: Observable<Array<AeronefByImat>> = this._aeronefsService.getAllByImat().pipe(takeUntilDestroyed(this.destroyRef));
this._aeronefByImat = aeronefs$.subscribe((aggregate: Array<AeronefByImat>) => { this._aeronefByImat = aeronefs$.subscribe((aggregate: Array<AeronefByImat>) => {
this._aeronefsByImat = aggregate.map((row: AeronefByImat) => { this._aeronefsByImat = aggregate.map((row: AeronefByImat) => {
this.chartConfig.doughnutChartLabels.push(`${row._id.aeronef} ${row._id.imat}`); this.seriesName.push(`${row.aeronef} ${row.imat}`);
this.chartConfig.doughnutChartDatasets[0].data.push(row.count); this.seriesValue.push(row.count);
return row; return row;
}); });
this.seriesName = [...this.chartConfig.doughnutChartLabels];
this.seriesValue = [...this.chartConfig.doughnutChartDatasets[0].data];
this.displayCharts = true;
this._loadAeronefByYear(); this._loadAeronefByYear();
}); });
} }
private _loadAeronefByYear(): void { private _loadAeronefByYear(): void {
this.seriesHeader = ["2018", "2019", "2020", "2021", "2022", "2023"]; this.seriesHeader = ['2018', '2019', '2020', '2021', '2022', '2023', '2024'];
let values: Array<number> = this.seriesHeader.map(() => 0); const values: Array<number> = this.seriesHeader.map(() => 0);
this.seriesName.forEach(() => { this.seriesName.forEach(() => {
this.seriesRow.push([...values]); this.seriesRow.push([...values]);
}); });
const dropzones$: Observable<Array<AeronefByYear>> = this._aeronefsService.getAllByDate(); const dropzones$: Observable<Array<AeronefByYear>> = this._aeronefsService.getAllByImatByYear().pipe(takeUntilDestroyed(this.destroyRef));
this._aeronefByYear = dropzones$.subscribe((aggregate: Array<AeronefByYear>) => { this._aeronefByYear = dropzones$.subscribe((aggregate: Array<AeronefByYear>) => {
this._aeronefsByYear = aggregate.map((row: AeronefByYear) => { this._aeronefsByYear = aggregate.map((row: AeronefByYear) => {
let aeronef: string = row._id.aeronef; const aeronef: string = row.aeronef;
let imat: string = row._id.imat; const imat: string = row.imat;
let year: string = row._id.year.toString(); const year: string = row.year.toString();
this.seriesRow[this.seriesName.indexOf(`${aeronef} ${imat}`)][this.seriesHeader.indexOf(year)] = row.count; this.seriesRow[this.seriesName.indexOf(`${aeronef} ${imat}`)][this.seriesHeader.indexOf(year)] = row.count;
return row; return row;
}); });
@@ -1,37 +1,31 @@
<mat-card> <mat-card>
<mat-card-header> <mat-card-header>
<mat-card-title>{{ title }}</mat-card-title> <mat-card-title>{{ title }}</mat-card-title>
<mat-card-subtitle>{{ subtitle }}</mat-card-subtitle> <mat-card-subtitle>{{ subtitle }}</mat-card-subtitle>
<span class="flex-spacer"></span> <span class="flex-spacer"></span>
<button mat-icon-button [matMenuTriggerFor]="menuCanopyModel" aria-label="Menu Canopy Model"> <button mat-icon-button [matMenuTriggerFor]="menuCanopyModel" aria-label="Menu Canopy Model">
<mat-icon fontIcon="more_vert"></mat-icon> <mat-icon fontIcon="more_vert"></mat-icon>
</button> </button>
<mat-menu #menuCanopyModel="matMenu"> <mat-menu #menuCanopyModel="matMenu">
<button mat-menu-item *ngFor="let menuitem of menuItems.getMenuPanel()"> @for (menuitem of menuItems.getMenuPanel(); track menuitem) {
<mat-icon [fontIcon]="menuitem.icon"></mat-icon> <button mat-menu-item>
<span>{{ menuitem.name }}</span> <mat-icon [fontIcon]="menuitem.icon"></mat-icon>
</button> <span>{{ menuitem.name }}</span>
</mat-menu> </button>
</mat-card-header> }
<hr> </mat-menu>
<mat-card-content> </mat-card-header>
<div *ngIf="displayCharts" class="piechart position-relative w-100 my-1"> <mat-divider class="my-3"></mat-divider>
<canvas baseChart class="mx-auto" <mat-card-content>
[labels]="chartConfig.doughnutChartLabels" <app-pie-chart [headers]="seriesHeader" [names]="seriesName" [values]="seriesValue" [colors]="seriesColor"></app-pie-chart>
[datasets]="chartConfig.doughnutChartDatasets" </mat-card-content>
[options]="chartConfig.doughnutChartOptions" <mat-divider class="mt-3 mb-0"></mat-divider>
[legend]="chartConfig.doughnutChartLegend" <mat-accordion>
[type]="'doughnut'"> <mat-expansion-panel>
</canvas> <mat-expansion-panel-header>
</div> <mat-panel-title>Historique annuel</mat-panel-title>
</mat-card-content> </mat-expansion-panel-header>
<hr class="mb-0"> <app-history-table [headers]="seriesHeader" [names]="seriesName" [values]="seriesValue" [rows]="seriesRow" [colors]="seriesColorClass"></app-history-table>
<mat-accordion> </mat-expansion-panel>
<mat-expansion-panel> </mat-accordion>
<mat-expansion-panel-header> </mat-card>
<mat-panel-title>Historique annuel</mat-panel-title>
</mat-expansion-panel-header>
<huapp-history-table [headers]="seriesHeader" [names]="seriesName" [values]="seriesValue" [rows]="seriesRow" [colors]="seriesColor"></huapp-history-table>
</mat-expansion-panel>
</mat-accordion>
</mat-card>
@@ -7,9 +7,9 @@ describe('CanopyModelsComponent', () => {
let fixture: ComponentFixture<CanopyModelsComponent>; let fixture: ComponentFixture<CanopyModelsComponent>;
beforeEach(async(() => { beforeEach(async(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [ CanopyModelsComponent ] imports: [CanopyModelsComponent]
}) })
.compileComponents(); .compileComponents();
})); }));

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