4 Commits

Author SHA1 Message Date
Rampeur 25d47bc3be Linting 2024-05-09 02:21:19 +02:00
Rampeur 276da97003 Mise à jour angular 2024-05-09 00:06:04 +02:00
Rampeur d1870adff6 Refactoring 2024-05-08 23:46:08 +02:00
Rampeur 7c3f0c9648 Implémentation SkydiverId API 2024-05-08 20:32:49 +02:00
123 changed files with 3113 additions and 2481 deletions
+543 -463
View File
File diff suppressed because it is too large Load Diff
+10 -9
View File
@@ -25,30 +25,31 @@
"@angular/common": "^17.3.5", "@angular/common": "^17.3.5",
"@angular/compiler": "^17.3.5", "@angular/compiler": "^17.3.5",
"@angular/core": "^17.3.5", "@angular/core": "^17.3.5",
"@angular/forms": "^17.3.5", "@angular/forms": "^17.3.8",
"@angular/localize": "^17.3.5", "@angular/google-maps": "^17.3.8",
"@angular/material": "^17.3.6", "@angular/localize": "^17.3.8",
"@angular/material": "^17.3.8",
"@angular/platform-browser": "^17.3.5", "@angular/platform-browser": "^17.3.5",
"@angular/platform-browser-dynamic": "^17.3.5", "@angular/platform-browser-dynamic": "^17.3.8",
"@angular/router": "^17.3.5", "@angular/router": "^17.3.8",
"bootstrap": "^5.3.3", "bootstrap": "^5.3.3",
"chart.js": "^4.4.2", "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": "^6.0.0", "ng2-charts": "^6.0.1",
"ngx-skeleton-loader": "^9.0.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.5"
}, },
"devDependencies": { "devDependencies": {
"@angular-devkit/build-angular": "^17.3.5", "@angular-devkit/build-angular": "^17.3.7",
"@angular-eslint/builder": "17.3.0", "@angular-eslint/builder": "17.3.0",
"@angular-eslint/eslint-plugin": "17.3.0", "@angular-eslint/eslint-plugin": "17.3.0",
"@angular-eslint/eslint-plugin-template": "17.3.0", "@angular-eslint/eslint-plugin-template": "17.3.0",
"@angular-eslint/schematics": "17.3.0", "@angular-eslint/schematics": "17.3.0",
"@angular-eslint/template-parser": "17.3.0", "@angular-eslint/template-parser": "17.3.0",
"@angular/cli": "^17.3.5", "@angular/cli": "^17.3.7",
"@angular/compiler-cli": "^17.3.5", "@angular/compiler-cli": "^17.3.5",
"@types/jasmine": "~5.1.0", "@types/jasmine": "~5.1.0",
"@typescript-eslint/eslint-plugin": "7.2.0", "@typescript-eslint/eslint-plugin": "7.2.0",
@@ -1,51 +0,0 @@
<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>
@@ -1,23 +0,0 @@
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,49 @@
<div @flyInOut>
<div class="d-flex">
<div class="flex-fill">
<h1 class="mb-1">{{title}}</h1>
<span [hidden]="!lastJump.numero" class="me-2 fs-6">
Dernier saut enregistré : <span class="fs-5 mx-1 text-primary">{{ lastJump.numero }}<sup>ème</sup></span> à <span class="fs-5 mx-1 text-primary">{{ lastJump.lieu }}</span> le <span class="fs-5 mx-1 text-primary">{{ lastJump.date | date: 'dd/MM/yyyy' }}</span>
</span>
</div>
<span class="flex-spacer"></span>
<div class="align-self-center me-2">
<button mat-icon-button [matMenuTriggerFor]="menuAeronef" aria-label="Menu Aeronef">
<mat-icon fontIcon="more_vert"></mat-icon>
</button>
<mat-menu #menuAeronef="matMenu">
@for (menuitem of menuItems.getMenuPanel(); track menuitem) {
<button mat-menu-item>
<mat-icon [fontIcon]="menuitem.icon"></mat-icon>
<span>{{ menuitem.name }}</span>
</button>
}
</mat-menu>
</div>
</div>
<mat-divider class="my-3"></mat-divider>
<mat-card>
<mat-card-header>
<mat-card-title>{{ subtitle }}</mat-card-title>
</mat-card-header>
<mat-card-content class="mt-4">
@if (displayCharts) {
<div class="row">
<div class="col-xl-4 col-lg-5 col-md-6 col-sm-12">
<app-pie-chart [headers]="seriesHeader" [names]="seriesName" [values]="seriesValue" [colors]="seriesColor"></app-pie-chart>
</div>
<div class="col-xl-8 col-lg-7 col-md-6 col-sm-12">
<!--<app-bar-chart [headers]="seriesHeader" [names]="seriesName" [values]="seriesValue" [colors]="seriesColor" label="Nombre total de sauts" [legend]="false"></app-bar-chart>-->
<app-bars-chart [headers]="seriesHeader" [names]="seriesName" [values]="seriesRow" [colors]="seriesColor" [legend]="false"></app-bars-chart>
</div>
</div>
}
<div class="d-flex">
<div class="flex-fill py-2">
<mat-divider class="mt-3 mb-0"></mat-divider>
<app-history-table [headers]="seriesHeader" [names]="seriesName" [values]="seriesValue" [rows]="seriesRow" [colors]="seriesColorClass"></app-history-table>
</div>
</div>
</mat-card-content>
</mat-card>
</div>
@@ -1,4 +1,4 @@
import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing'; import { ComponentFixture, TestBed } from '@angular/core/testing';
import { AeronefsComponent } from './aeronefs.component'; import { AeronefsComponent } from './aeronefs.component';
@@ -6,14 +6,12 @@ describe('AeronefsComponent', () => {
let component: AeronefsComponent; let component: AeronefsComponent;
let fixture: ComponentFixture<AeronefsComponent>; let fixture: ComponentFixture<AeronefsComponent>;
beforeEach(waitForAsync(() => { beforeEach(async () => {
TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [AeronefsComponent] imports: [AeronefsComponent]
}) })
.compileComponents(); .compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(AeronefsComponent); fixture = TestBed.createComponent(AeronefsComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
fixture.detectChanges(); fixture.detectChanges();
@@ -21,7 +21,7 @@ import { UtilitiesService } from 'src/app/core/services';
import { AeronefByImat, AeronefByYear } from 'src/app/core/models'; import { AeronefByImat, AeronefByYear } from 'src/app/core/models';
@Component({ @Component({
selector: 'app-aeronef', selector: 'app-aeronefs',
standalone: true, standalone: true,
imports: [ imports: [
DatePipe, DatePipe,
@@ -29,8 +29,8 @@ import { AeronefByImat, AeronefByYear } from 'src/app/core/models';
MatExpansionModule, MatGridListModule, MatIconModule, MatMenuModule, MatExpansionModule, MatGridListModule, MatIconModule, MatMenuModule,
BarsChartComponent, HistoryTableComponent, PieChartComponent BarsChartComponent, HistoryTableComponent, PieChartComponent
], ],
templateUrl: './aeronef.component.html', templateUrl: './aeronefs.component.html',
styleUrl: './aeronef.component.scss', styleUrl: './aeronefs.component.scss',
animations: [ animations: [
trigger('flyInOut', [ trigger('flyInOut', [
state('in', style({ transform: 'translateX(0)' })), state('in', style({ transform: 'translateX(0)' })),
@@ -45,7 +45,7 @@ import { AeronefByImat, AeronefByYear } from 'src/app/core/models';
]) ])
] ]
}) })
export class AeronefComponent implements OnInit, OnDestroy { export class AeronefsComponent implements OnInit, OnDestroy {
private _data: Subscription = new Subscription(); private _data: Subscription = new Subscription();
private _aeronefsByImat!: Array<AeronefByImat>; private _aeronefsByImat!: Array<AeronefByImat>;
private _aeronefsByYear!: Array<AeronefByYear>; private _aeronefsByYear!: Array<AeronefByYear>;
+1 -1
View File
@@ -1,4 +1,4 @@
<div class="container content auth-page"> <div class="container auth-page">
<div class="row"> <div class="row">
<div class="col-md-6 offset-md-3 col-xs-12"> <div class="col-md-6 offset-md-3 col-xs-12">
<h1 class="text-xs-center">{{ title }}</h1> <h1 class="text-xs-center">{{ title }}</h1>
@@ -1,259 +1,257 @@
<div @flyInOut> <div @flyInOut>
<div class="calculator-page content"> <div class="d-flex">
<div class="d-flex"> <div class="flex-fill">
<div class="flex-fill"> <h1 class="mb-1">{{title}}</h1>
<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>
<span class="me-2 fs-6"><span class="fs-5 mx-1">DT48</span> - 08 février 2024</span> </div>
</div> <span class="flex-spacer"></span>
<span class="flex-spacer"></span> <div class="align-self-center me-2">
<div class="align-self-center me-2"> <button mat-icon-button [matMenuTriggerFor]="menuCalculator" aria-label="Menu Calculator">
<button mat-icon-button [matMenuTriggerFor]="menuCalculator" aria-label="Menu Calculator"> <mat-icon>more_vert</mat-icon>
<mat-icon>more_vert</mat-icon> </button>
</button> <mat-menu #menuCalculator="matMenu">
<mat-menu #menuCalculator="matMenu"> @for (menuitem of menuItems.getMenuCalculator(); track menuitem) {
@for (menuitem of menuItems.getMenuCalculator(); track menuitem) { @if (menuitem.type === 'link') {
@if (menuitem.type === 'link') { <button mat-menu-item [routerLink]="['/', menuitem.state]">
<button mat-menu-item [routerLink]="['/', menuitem.state]"> <mat-icon [fontIcon]="menuitem.icon"></mat-icon>
<mat-icon [fontIcon]="menuitem.icon"></mat-icon> <span>{{ menuitem.name }}</span>
<span>{{ menuitem.name }}</span> </button>
</button> }
} }
} </mat-menu>
</mat-menu>
</div>
</div> </div>
<mat-divider class="my-3"></mat-divider>
<mat-card class="calculator">
<mat-card-header class="bg-warn rounded-top pt-1 pb-2 px-2">
<form #searchFormData="ngForm" class="inputs-form">
<mat-form-field class="bg-crystal me-3" subscriptSizing="dynamic" color="accent">
<mat-label>Nombre de sauts</mat-label>
<input matInput [(ngModel)]="inputs.jumps" (change)="refresh()" type="number" min="0" max="2000"
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>
<mat-divider class="my-3"></mat-divider>
<h4 class="py-2 mb-0">Tailles de voile pour {{ inputs.weight }} kg</h4>
<div>
Prochain changement de valeurs dans {{ ((this.currentRange.end + 1) - inputs.jumps) }}
{{ ((this.currentRange.end + 1) - inputs.jumps) > 1 ? 'sauts' : 'saut'}}<br />
</div>
<div class="text-end">
{{ ((((inputs.jumps - this.currentRange.start) + 1) / ((this.currentRange.end - this.currentRange.start) + 1)) * 100) | number : '1.2-2' }}%
de la tranche actuelle ({{this.currentRange.start}} à {{this.currentRange.end}})
</div>
<mat-progress-bar mode="determinate"
[value]="((((inputs.jumps - this.currentRange.start) + 1) / ((this.currentRange.end - this.currentRange.start) + 1)) * 100)"
color="warn"></mat-progress-bar>
<table class="table table-striped table-dark table-hover mat-elevation-z2 mb-1">
<thead>
<tr class="warn">
<th class="first"></th>
@for (header of sizesHeader; track header; let index = $index) {
<th class="pl-3 pr-3 {{ tableHeader[(index+1)].active }}">{{ header.name }}</th>
}
</tr>
</thead>
<tbody>
<tr class="warn">
<th class="first">Taille de voile en ft<sup>2</sup></th>
@for (value of sizesValuesFeet; track value; let index = $index) {
<td class="pl-3 text-nowrap {{ tableHeader[(index+1)].active }}">{{ value }}</td>
}
</tr>
<tr class="warn">
<th class="first">Abbattement 11%</th>
@for (value of sizesMinValuesFeet; track value; let index = $index) {
<td class="pl-3 text-nowrap {{ tableHeader[(index+1)].active }}">{{ value }}</td>
}
</tr>
<tr class="warn">
<th class="first">Charge alaire</th>
@for (charge of sizesCharges; track charge; let index = $index) {
<td class="pl-3 text-nowrap {{ tableHeader[(index+1)].active }}">
{{ charge | number : '1.2-3' }}</td>
}
</tr>
<tr class="warn">
<th class="first">Taille de voile en m<sup>2</sup></th>
@for (value of sizesValuesMeter; track value; let index = $index) {
<td class="pl-3 text-nowrap {{ tableHeader[(index+1)].active }}">
{{ value | number : '1.2-2' }}</td>
}
</tr>
</tbody>
</table>
<mat-divider class="my-3"></mat-divider>
<h4 class="py-2 mb-0">Tableau des tailles de voile par poids</h4>
<div class="text-end">
{{ ((inputs.jumps / 1600) * 100) | number : '1.2-2' }}% de la plage complète (0 à 1600)
</div>
<mat-progress-bar mode="determinate" [value]="((inputs.jumps / 1600) * 100)" color="warn">
</mat-progress-bar>
<table mat-table [dataSource]="canopy_size_table" matSort
class="table table-striped table-dark table-hover mat-elevation-z2">
<!-- weight column -->
<ng-container matColumnDef="weight">
<th mat-header-cell *matHeaderCellDef mat-sort-header class="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)">
<span class="text-warn d-block">{{element.weight}}</span>
</th>
</ng-container>
<!-- range_1 column -->
<ng-container matColumnDef="range_1">
<th class="pl-3" mat-header-cell *matHeaderCellDef class="{{tableHeader[1].active}}">
{{tableHeader[1].name}} </th>
<td mat-cell *matCellDef="let element;"
class="pl-3 text-nowrap fs-6 {{element.ranges[0].active}}">{{element.ranges[0].value}}</td>
</ng-container>
<!-- range_2 column -->
<ng-container matColumnDef="range_2">
<th class="pl-3" mat-header-cell *matHeaderCellDef class="{{tableHeader[2].active}}">
{{tableHeader[2].name}} </th>
<td mat-cell *matCellDef="let element;"
class="pl-3 text-nowrap fs-6 {{element.ranges[1].active}}">{{element.ranges[1].value}}</td>
</ng-container>
<!-- range_3 column -->
<ng-container matColumnDef="range_3">
<th class="pl-3" mat-header-cell *matHeaderCellDef class="{{tableHeader[3].active}}">
{{tableHeader[3].name}} </th>
<td mat-cell *matCellDef="let element;"
class="pl-3 text-nowrap fs-6 {{element.ranges[2].active}}">{{element.ranges[2].value}}</td>
</ng-container>
<!-- range_4 column -->
<ng-container matColumnDef="range_4">
<th class="pl-3" mat-header-cell *matHeaderCellDef class="{{tableHeader[4].active}}">
{{tableHeader[4].name}} </th>
<td mat-cell *matCellDef="let element;"
class="pl-3 text-nowrap fs-6 {{element.ranges[3].active}}">{{element.ranges[3].value}}</td>
</ng-container>
<!-- range_5 column -->
<ng-container matColumnDef="range_5">
<th class="pl-3" mat-header-cell *matHeaderCellDef class="{{tableHeader[5].active}}">
{{tableHeader[5].name}} </th>
<td mat-cell *matCellDef="let element;"
class="pl-3 text-nowrap fs-6 {{element.ranges[4].active}}">{{element.ranges[4].value}}</td>
</ng-container>
<!-- range_6 column -->
<ng-container matColumnDef="range_6">
<th class="pl-3" mat-header-cell *matHeaderCellDef class="{{tableHeader[6].active}}">
{{tableHeader[6].name}} </th>
<td mat-cell *matCellDef="let element;"
class="pl-3 text-nowrap fs-6 {{element.ranges[5].active}}">{{element.ranges[5].value}}</td>
</ng-container>
<!-- range_7 column -->
<ng-container matColumnDef="range_7">
<th class="pl-3" mat-header-cell *matHeaderCellDef class="{{tableHeader[7].active}}">
{{tableHeader[7].name}} </th>
<td mat-cell *matCellDef="let element;"
class="pl-3 text-nowrap fs-6 {{element.ranges[6].active}}">{{element.ranges[6].value}}</td>
</ng-container>
<!-- range_8 column -->
<ng-container matColumnDef="range_8">
<th class="pl-3" mat-header-cell *matHeaderCellDef class="{{tableHeader[8].active}}">
{{tableHeader[8].name}} </th>
<td mat-cell *matCellDef="let element;"
class="pl-3 text-nowrap fs-6 {{element.ranges[7].active}}">{{element.ranges[7].value}}</td>
</ng-container>
<!-- range_9 column -->
<ng-container matColumnDef="range_9">
<th class="pl-3" mat-header-cell *matHeaderCellDef class="{{tableHeader[9].active}}">
{{tableHeader[9].name}} </th>
<td mat-cell *matCellDef="let element;"
class="pl-3 text-nowrap fs-6 {{element.ranges[8].active}}">{{element.ranges[8].value}}</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns; sticky: false" class="warn"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"
class="text-middle font-monospace warn {{row.active}}" id="weight_{{row.weight}}"></tr>
</table>
<div class="clearfix">
<mat-paginator [pageSize]="50" [pageSizeOptions]="[5, 10, 15, 25, 50]" showFirstLastButtons
class="pull-right mb-1 mt-2"></mat-paginator>
</div>
</mat-card-content>
</mat-card>
</div> </div>
<mat-divider class="my-3"></mat-divider>
<mat-card class="calculator">
<mat-card-header class="bg-warn rounded-top pt-1 pb-2 px-2">
<form #searchFormData="ngForm" class="inputs-form">
<mat-form-field class="bg-crystal me-3" subscriptSizing="dynamic" color="accent">
<mat-label>Nombre de sauts</mat-label>
<input matInput [(ngModel)]="inputs.jumps" (change)="refresh()" type="number" min="0" max="2000"
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="text-nowrap pe-4"></th>
<th class="text-nowrap">Taille actuelle</th>
<th class="text-nowrap">Taille min</th>
<th class="text-nowrap">Taille min -11%</th>
</tr>
</thead>
<tbody>
<tr class="warn">
<th class="text-nowrap pe-4">Taille de voile en ft<sup>2</sup></th>
<td class="font-monospace text-{{ info.state.color }}">
{{ info.sizesFeet.current }} </td>
<td class="font-monospace text-success"> ● {{ info.sizesFeet.min }} </td>
<td class="font-monospace text-warning"> ● {{ info.sizesFeet.min11 }} </td>
</tr>
<tr class="warn">
<th class="text-nowrap pe-4">Charge alaire</th>
<td class="font-monospace">{{ info.charges.current | number : '1.2-3' }}</td>
<td class="font-monospace">{{ info.charges.min | number : '1.2-3' }}</td>
<td class="font-monospace">{{ info.charges.min11 | number : '1.2-3' }}</td>
</tr>
<tr class="warn">
<th class="text-nowrap pe-4">Taille de voile en m<sup>2</sup></th>
<td class="font-monospace">{{ info.sizesMeter.current | number : '1.2-3' }}</td>
<td class="font-monospace">{{ info.sizesMeter.min | number : '1.2-3' }}</td>
<td class="font-monospace">{{ info.sizesMeter.min11 | number : '1.2-3' }}</td>
</tr>
<tr class="warn">
<th class="text-nowrap pe-4">Coefficient kilo/livres</th>
<td colspan="3" class="font-monospace text-start">{{ coeffKgLbs }}</td>
</tr>
<tr class="warn">
<th class="text-nowrap pe-4">Coefficient pied/mètre</th>
<td colspan="3" class="font-monospace text-start">{{ coeffFtM }}</td>
</tr>
<tr class="warn">
<th class="text-nowrap pe-4">Formule charge alaire</th>
<td colspan="3" class="font-monospace text-start">
( ( {{ inputs.weight }} + {{ inputs.gear }} ) * {{ coeffKgLbs }} ) /
{{ inputs.current }} = {{ info.charges.current | number : '1.5' }}<br />
( ( poids nu en kg + poids equipement en kg ) * coefficient kg/lbs ) / taille de
voile en ft<sup>2</sup>
</td>
</tr>
</tbody>
</table>
</div>
<div 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>
<mat-divider class="my-3"></mat-divider>
<h4 class="py-2 mb-0">Tailles de voile pour {{ inputs.weight }} kg</h4>
<div>
Prochain changement de valeurs dans {{ ((this.currentRange.end + 1) - inputs.jumps) }}
{{ ((this.currentRange.end + 1) - inputs.jumps) > 1 ? 'sauts' : 'saut'}}<br />
</div>
<div class="text-end">
{{ ((((inputs.jumps - this.currentRange.start) + 1) / ((this.currentRange.end - this.currentRange.start) + 1)) * 100) | number : '1.2-2' }}%
de la tranche actuelle ({{this.currentRange.start}} à {{this.currentRange.end}})
</div>
<mat-progress-bar mode="determinate"
[value]="((((inputs.jumps - this.currentRange.start) + 1) / ((this.currentRange.end - this.currentRange.start) + 1)) * 100)"
color="warn"></mat-progress-bar>
<table class="table table-striped table-dark table-hover mat-elevation-z2 mb-1">
<thead>
<tr class="warn">
<th class="text-nowrap pe-4"></th>
@for (header of sizesHeader; track header; let index = $index) {
<th class="pl-3 pr-3 {{ tableHeader[(index+1)].active }}">{{ header.name }}</th>
}
</tr>
</thead>
<tbody>
<tr class="warn">
<th class="firtext-nowrap pe-4st">Taille de voile en ft<sup>2</sup></th>
@for (value of sizesValuesFeet; track value; let index = $index) {
<td class="pl-3 text-nowrap {{ tableHeader[(index+1)].active }}">{{ value }}</td>
}
</tr>
<tr class="warn">
<th class="text-nowrap pe-4">Abbattement 11%</th>
@for (value of sizesMinValuesFeet; track value; let index = $index) {
<td class="pl-3 text-nowrap {{ tableHeader[(index+1)].active }}">{{ value }}</td>
}
</tr>
<tr class="warn">
<th class="text-nowrap pe-4">Charge alaire</th>
@for (charge of sizesCharges; track charge; let index = $index) {
<td class="pl-3 text-nowrap {{ tableHeader[(index+1)].active }}">
{{ charge | number : '1.2-3' }}</td>
}
</tr>
<tr class="warn">
<th class="text-nowrap pe-4">Taille de voile en m<sup>2</sup></th>
@for (value of sizesValuesMeter; track value; let index = $index) {
<td class="pl-3 text-nowrap {{ tableHeader[(index+1)].active }}">
{{ value | number : '1.2-2' }}</td>
}
</tr>
</tbody>
</table>
<mat-divider class="my-3"></mat-divider>
<h4 class="py-2 mb-0">Tableau des tailles de voile par poids</h4>
<div class="text-end">
{{ ((inputs.jumps / 1600) * 100) | number : '1.2-2' }}% de la plage complète (0 à 1600)
</div>
<mat-progress-bar mode="determinate" [value]="((inputs.jumps / 1600) * 100)" color="warn">
</mat-progress-bar>
<table mat-table [dataSource]="canopy_size_table" matSort
class="table table-striped table-dark table-hover mat-elevation-z2">
<!-- weight column -->
<ng-container matColumnDef="weight">
<th mat-header-cell *matHeaderCellDef mat-sort-header class="text-nowrap pe-4 {{tableHeader[0].active}}">
{{tableHeader[0].name}} </th>
<th mat-cell *matCellDef="let element;" class="text-end ps-3 pe-4 fs-6" role="button"
(click)="setCurrentWeight(element.weight)">
<span class="text-warn d-block">{{element.weight}}</span>
</th>
</ng-container>
<!-- range_1 column -->
<ng-container matColumnDef="range_1">
<th class="pl-3" mat-header-cell *matHeaderCellDef class="{{tableHeader[1].active}}">
{{tableHeader[1].name}} </th>
<td mat-cell *matCellDef="let element;"
class="pl-3 text-nowrap fs-6 {{element.ranges[0].active}}">{{element.ranges[0].value}}</td>
</ng-container>
<!-- range_2 column -->
<ng-container matColumnDef="range_2">
<th class="pl-3" mat-header-cell *matHeaderCellDef class="{{tableHeader[2].active}}">
{{tableHeader[2].name}} </th>
<td mat-cell *matCellDef="let element;"
class="pl-3 text-nowrap fs-6 {{element.ranges[1].active}}">{{element.ranges[1].value}}</td>
</ng-container>
<!-- range_3 column -->
<ng-container matColumnDef="range_3">
<th class="pl-3" mat-header-cell *matHeaderCellDef class="{{tableHeader[3].active}}">
{{tableHeader[3].name}} </th>
<td mat-cell *matCellDef="let element;"
class="pl-3 text-nowrap fs-6 {{element.ranges[2].active}}">{{element.ranges[2].value}}</td>
</ng-container>
<!-- range_4 column -->
<ng-container matColumnDef="range_4">
<th class="pl-3" mat-header-cell *matHeaderCellDef class="{{tableHeader[4].active}}">
{{tableHeader[4].name}} </th>
<td mat-cell *matCellDef="let element;"
class="pl-3 text-nowrap fs-6 {{element.ranges[3].active}}">{{element.ranges[3].value}}</td>
</ng-container>
<!-- range_5 column -->
<ng-container matColumnDef="range_5">
<th class="pl-3" mat-header-cell *matHeaderCellDef class="{{tableHeader[5].active}}">
{{tableHeader[5].name}} </th>
<td mat-cell *matCellDef="let element;"
class="pl-3 text-nowrap fs-6 {{element.ranges[4].active}}">{{element.ranges[4].value}}</td>
</ng-container>
<!-- range_6 column -->
<ng-container matColumnDef="range_6">
<th class="pl-3" mat-header-cell *matHeaderCellDef class="{{tableHeader[6].active}}">
{{tableHeader[6].name}} </th>
<td mat-cell *matCellDef="let element;"
class="pl-3 text-nowrap fs-6 {{element.ranges[5].active}}">{{element.ranges[5].value}}</td>
</ng-container>
<!-- range_7 column -->
<ng-container matColumnDef="range_7">
<th class="pl-3" mat-header-cell *matHeaderCellDef class="{{tableHeader[7].active}}">
{{tableHeader[7].name}} </th>
<td mat-cell *matCellDef="let element;"
class="pl-3 text-nowrap fs-6 {{element.ranges[6].active}}">{{element.ranges[6].value}}</td>
</ng-container>
<!-- range_8 column -->
<ng-container matColumnDef="range_8">
<th class="pl-3" mat-header-cell *matHeaderCellDef class="{{tableHeader[8].active}}">
{{tableHeader[8].name}} </th>
<td mat-cell *matCellDef="let element;"
class="pl-3 text-nowrap fs-6 {{element.ranges[7].active}}">{{element.ranges[7].value}}</td>
</ng-container>
<!-- range_9 column -->
<ng-container matColumnDef="range_9">
<th class="pl-3" mat-header-cell *matHeaderCellDef class="{{tableHeader[9].active}}">
{{tableHeader[9].name}} </th>
<td mat-cell *matCellDef="let element;"
class="pl-3 text-nowrap fs-6 {{element.ranges[8].active}}">{{element.ranges[8].value}}</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns; sticky: false" class="warn"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"
class="text-middle font-monospace warn {{row.active}}" id="weight_{{row.weight}}"></tr>
</table>
<div class="clearfix">
<mat-paginator [pageSize]="50" [pageSizeOptions]="[5, 10, 15, 25, 50]" showFirstLastButtons
class="pull-right mb-1 mt-2"></mat-paginator>
</div>
</mat-card-content>
</mat-card>
</div> </div>
@@ -0,0 +1,56 @@
<div @flyInOut>
<div class="d-flex">
<div class="flex-fill">
<h1 class="mb-1">{{title}}</h1>
<span [hidden]="!lastJump.numero" class="me-2 fs-6">
Dernier saut enregistré : <span class="fs-5 mx-1 text-primary">{{ lastJump.numero }}<sup>ème</sup></span> à <span class="fs-5 mx-1 text-primary">{{ lastJump.lieu }}</span> le <span class="fs-5 mx-1 text-primary">{{ lastJump.date | date: 'dd/MM/yyyy' }}</span>
</span>
</div>
<span class="flex-spacer"></span>
<div class="align-self-center me-2">
<button mat-icon-button [matMenuTriggerFor]="menuCanopyModel" aria-label="Menu Canopy Model">
<mat-icon fontIcon="more_vert"></mat-icon>
</button>
<mat-menu #menuCanopyModel="matMenu">
@for (menuitem of menuItems.getMenuPanel(); track menuitem) {
<button mat-menu-item>
<mat-icon [fontIcon]="menuitem.icon"></mat-icon>
<span>{{ menuitem.name }}</span>
</button>
}
</mat-menu>
</div>
</div>
<mat-divider class="my-3"></mat-divider>
<mat-card>
<mat-card-header>
<mat-card-title>{{ subtitle }}</mat-card-title>
</mat-card-header>
<mat-card-content class="mt-4">
@if (displayCharts) {
<div class="row">
<div class="col-xl-3 col-lg-6 col-md-6 col-sm-12">
<app-pie-chart [headers]="seriesModelHeader" [names]="seriesModelName" [values]="seriesModelValue" [colors]="seriesColor"></app-pie-chart>
</div>
<div class="col-xl-3 col-lg-6 col-md-6 col-sm-12">
<app-pie-chart [headers]="seriesSizeHeader" [names]="seriesSizeName" [values]="seriesSizeValue" [colors]="seriesColor"></app-pie-chart>
</div>
<div class="col-xl-6 col-lg-12 col-md-12">
<!--<app-pie-chart [headers]="seriesSizeHeader" [names]="seriesSizeName" [values]="seriesSizeValue" [colors]="seriesColor"></app-pie-chart>-->
<app-bars-chart [headers]="seriesSizeHeader" [names]="seriesSizeName" [values]="seriesSizeRow" [colors]="seriesColor" [legend]="false"></app-bars-chart>
</div>
</div>
}
<div class="row">
<div class="col-xl-6 col-lg-12">
<mat-divider class="mt-3 mb-0"></mat-divider>
<app-history-table [headers]="seriesModelHeader" [names]="seriesModelName" [values]="seriesModelValue" [rows]="seriesModelRow" [colors]="seriesColorClass"></app-history-table>
</div>
<div class="col-xl-6 col-lg-12">
<mat-divider class="mt-3 mb-0"></mat-divider>
<app-history-table [headers]="seriesSizeHeader" [names]="seriesSizeName" [values]="seriesSizeValue" [rows]="seriesSizeRow" [colors]="seriesColorClass"></app-history-table>
</div>
</div>
</mat-card-content>
</mat-card>
</div>
@@ -1,18 +1,18 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ComponentFixture, TestBed } from '@angular/core/testing';
import { CanopyComponent } from './canopy.component'; import { CanopiesComponent } from './canopies.component';
describe('CanopyComponent', () => { describe('CanopiesComponent', () => {
let component: CanopyComponent; let component: CanopiesComponent;
let fixture: ComponentFixture<CanopyComponent>; let fixture: ComponentFixture<CanopiesComponent>;
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [CanopyComponent] imports: [CanopiesComponent]
}) })
.compileComponents(); .compileComponents();
fixture = TestBed.createComponent(CanopyComponent); fixture = TestBed.createComponent(CanopiesComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
fixture.detectChanges(); fixture.detectChanges();
}); });
@@ -20,7 +20,7 @@ import { UtilitiesService } from 'src/app/core/services';
import { CanopyBySize, CanopyByYear, CanopyModelBySize, CanopyModelByYear } from 'src/app/core/models'; import { CanopyBySize, CanopyByYear, CanopyModelBySize, CanopyModelByYear } from 'src/app/core/models';
@Component({ @Component({
selector: 'app-canopy', selector: 'app-canopies',
standalone: true, standalone: true,
imports: [ imports: [
DatePipe, DatePipe,
@@ -28,8 +28,8 @@ import { CanopyBySize, CanopyByYear, CanopyModelBySize, CanopyModelByYear } from
MatGridListModule, MatIconModule, MatMenuModule, MatGridListModule, MatIconModule, MatMenuModule,
HistoryTableComponent, BarsChartComponent, PieChartComponent HistoryTableComponent, BarsChartComponent, PieChartComponent
], ],
templateUrl: './canopy.component.html', templateUrl: './canopies.component.html',
styleUrl: './canopy.component.scss', styleUrl: './canopies.component.scss',
animations: [ animations: [
trigger('flyInOut', [ trigger('flyInOut', [
state('in', style({ transform: 'translateX(0)' })), state('in', style({ transform: 'translateX(0)' })),
@@ -44,7 +44,7 @@ import { CanopyBySize, CanopyByYear, CanopyModelBySize, CanopyModelByYear } from
]) ])
] ]
}) })
export class CanopyComponent implements OnInit, OnDestroy { export class CanopiesComponent implements OnInit, OnDestroy {
private _data: Subscription = new Subscription(); private _data: Subscription = new Subscription();
private _canopiesModelBySize!: Array<CanopyModelBySize>; private _canopiesModelBySize!: Array<CanopyModelBySize>;
private _canopiesModelByYear!: Array<CanopyModelByYear>; private _canopiesModelByYear!: Array<CanopyModelByYear>;
@@ -1,58 +0,0 @@
<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>
@@ -1,38 +1,36 @@
<div class="content"> <h1 class="mb-1">{{title}}</h1>
<h1 class="mb-1">{{title}}</h1> <span [hidden]="!lastJump.numero" class="me-2 fst-italic0 fs-6">
<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>
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>
</span> <mat-divider class="my-3"></mat-divider>
<mat-divider class="my-3"></mat-divider> <ng-container *appShowAuthed="true">
<ng-container *appShowAuthed="true"> <div class="row">
<div class="row"> <div class="col-xs-12">
<div class="col-xs-12"> <app-jumps-by-month></app-jumps-by-month>
<app-jumps-by-month></app-jumps-by-month>
</div>
</div> </div>
<div class="row mt-sm-2 mt-lg-3"> </div>
<div class="col-md-4 col-sm-12"> <div class="row mt-sm-2 mt-lg-3">
<app-drop-zones></app-drop-zones> <div class="col-md-4 col-sm-12">
</div> <app-dropzones-pie></app-dropzones-pie>
<div class="col-md-8 col-sm-12">
<app-drop-zones-bar></app-drop-zones-bar>
</div>
</div> </div>
<div class="row mt-sm-2 mt-lg-3"> <div class="col-md-8 col-sm-12">
<div class="col-md-4 col-sm-12"> <app-dropzones-bar></app-dropzones-bar>
<app-aeronefs></app-aeronefs>
</div>
<div class="col-md-8 col-sm-12">
<app-aeronefs-bar></app-aeronefs-bar>
</div>
</div> </div>
<div class="row mt-sm-2 mt-lg-3"> </div>
<div class="col-md-6 col-sm-12"> <div class="row mt-sm-2 mt-lg-3">
<app-canopy-models></app-canopy-models> <div class="col-md-4 col-sm-12">
</div> <app-aeronefs-pie></app-aeronefs-pie>
<div class="col-md-6 col-sm-12">
<app-canopy-sizes></app-canopy-sizes>
</div>
</div> </div>
</ng-container> <div class="col-md-8 col-sm-12">
</div> <app-aeronefs-bar></app-aeronefs-bar>
</div>
</div>
<div class="row mt-sm-2 mt-lg-3">
<div class="col-md-6 col-sm-12">
<app-canopies-models></app-canopies-models>
</div>
<div class="col-md-6 col-sm-12">
<app-canopies-sizes></app-canopies-sizes>
</div>
</div>
</ng-container>
@@ -12,9 +12,9 @@ import { Aeronef, AeronefByImat, Canopy, CanopyModelBySize, DropZone, DropZoneBy
import { JumpsService, 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, AeronefsPieComponent, AeronefsBarComponent,
CanopyModelsComponent, CanopySizesComponent, CanopiesModelsComponent, CanopiesSizesComponent,
DropZonesComponent, DropZonesBarComponent, DropzonesPieComponent, DropzonesBarComponent,
JumpsByMonthComponent JumpsByMonthComponent
} from 'src/app/components/shared/dashboard-components'; } from 'src/app/components/shared/dashboard-components';
@@ -22,9 +22,9 @@ import {
standalone: true, standalone: true,
imports: [ imports: [
DatePipe, ShowAuthedDirective, MatDividerModule, DatePipe, ShowAuthedDirective, MatDividerModule,
AeronefsComponent, AeronefsBarComponent, AeronefsPieComponent, AeronefsBarComponent,
CanopyModelsComponent, CanopySizesComponent, CanopiesModelsComponent, CanopiesSizesComponent,
DropZonesComponent, DropZonesBarComponent, DropzonesPieComponent, DropzonesBarComponent,
JumpsByMonthComponent JumpsByMonthComponent
], ],
selector: 'app-dashboard', selector: 'app-dashboard',
@@ -1,57 +0,0 @@
<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,55 @@
<div @flyInOut>
<div class="d-flex">
<div class="flex-fill">
<h1 class="mb-1">{{title}}</h1>
<span [hidden]="!lastJump.numero" class="me-2 fs-6">
Dernier saut enregistré : <span class="fs-5 mx-1 text-primary">{{ lastJump.numero }}<sup>ème</sup></span> à <span class="fs-5 mx-1 text-primary">{{ lastJump.lieu }}</span> le <span class="fs-5 mx-1 text-primary">{{ lastJump.date | date: 'dd/MM/yyyy' }}</span>
</span>
</div>
<span class="flex-spacer"></span>
<div class="align-self-center me-2">
<button mat-icon-button [matMenuTriggerFor]="menuDropzone" aria-label="Menu Dropzone">
<mat-icon fontIcon="more_vert"></mat-icon>
</button>
<mat-menu #menuDropzone="matMenu">
@for (menuitem of menuItems.getMenuPanel(); track menuitem) {
<button mat-menu-item>
<mat-icon [fontIcon]="menuitem.icon"></mat-icon>
<span>{{ menuitem.name }}</span>
</button>
}
</mat-menu>
</div>
</div>
<mat-divider class="my-3"></mat-divider>
<mat-card>
<mat-card-header>
<mat-card-title>{{ subtitle }}</mat-card-title>
</mat-card-header>
<mat-card-content class="mt-4">
@if (displayCharts) {
<div class="row">
<div class="col-xl-4 col-lg-5 col-md-6 col-sm-12">
<app-pie-chart [headers]="seriesHeader" [names]="seriesName" [values]="seriesValue" [colors]="seriesColor"></app-pie-chart>
</div>
<div class="col-xl-8 col-lg-7 col-md-6 col-sm-12">
<app-bars-chart [headers]="seriesHeader" [names]="seriesName" [values]="seriesRow" [colors]="seriesColor" [legend]="false"></app-bars-chart>
<!--
@if (displayCharts) {
<div class="barchart position-relative w-100 my-1">
<x-chartist [configuration]="chartBarConfig"></x-chartist>
</div>
}
-->
</div>
</div>
}
<div class="d-flex">
<div class="flex-fill py-2">
<mat-divider class="mt-3 mb-0"></mat-divider>
<app-history-table [headers]="seriesHeader" [names]="seriesName" [values]="seriesValue" [rows]="seriesRow" [colors]="seriesColorClass"></app-history-table>
</div>
</div>
</mat-card-content>
</mat-card>
</div>
@@ -1,18 +1,18 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ComponentFixture, TestBed } from '@angular/core/testing';
import { DropzoneComponent } from './dropzone.component'; import { DropzonesComponent } from './dropzones.component';
describe('DropzoneComponent', () => { describe('DropzonesComponent', () => {
let component: DropzoneComponent; let component: DropzonesComponent;
let fixture: ComponentFixture<DropzoneComponent>; let fixture: ComponentFixture<DropzonesComponent>;
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [DropzoneComponent] imports: [DropzonesComponent]
}) })
.compileComponents(); .compileComponents();
fixture = TestBed.createComponent(DropzoneComponent); fixture = TestBed.createComponent(DropzonesComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
fixture.detectChanges(); fixture.detectChanges();
}); });
@@ -21,7 +21,7 @@ import { DropZoneByOaci, DropZoneByYear, DropZonesPageData, Jump } from 'src/app
import { UtilitiesService } from 'src/app/core/services'; import { UtilitiesService } from 'src/app/core/services';
@Component({ @Component({
selector: 'app-dropzone', selector: 'app-dropzones',
standalone: true, standalone: true,
imports: [ imports: [
DatePipe, DatePipe,
@@ -30,8 +30,8 @@ import { UtilitiesService } from 'src/app/core/services';
//ChartistModule, //ChartistModule,
HistoryTableComponent, BarsChartComponent, PieChartComponent HistoryTableComponent, BarsChartComponent, PieChartComponent
], ],
templateUrl: './dropzone.component.html', templateUrl: './dropzones.component.html',
styleUrl: './dropzone.component.scss', styleUrl: './dropzones.component.scss',
animations: [ animations: [
trigger('flyInOut', [ trigger('flyInOut', [
state('in', style({ transform: 'translateX(0)' })), state('in', style({ transform: 'translateX(0)' })),
@@ -46,7 +46,7 @@ import { UtilitiesService } from 'src/app/core/services';
]) ])
] ]
}) })
export class DropzoneComponent implements OnInit, OnDestroy { export class DropzonesComponent implements OnInit, OnDestroy {
private _data: Subscription = new Subscription(); private _data: Subscription = new Subscription();
private _lastjump: Subscription = new Subscription(); private _lastjump: Subscription = new Subscription();
private _dropzonesByOaci!: Array<DropZoneByOaci>; private _dropzonesByOaci!: Array<DropZoneByOaci>;
File diff suppressed because one or more lines are too long
@@ -0,0 +1,17 @@
/* Home */
//@use 'variable' as var;
/*
#FF41F8
#F0060B
#F0070C
#CC26D5
#7702FF
#35A2EB
#B7DDD0
#B0D9CB
#D9CAAE
#9788C7
*/
@@ -41,6 +41,8 @@ export class HomeComponent implements OnInit {
public errors: Errors = { errors: {} }; public errors: Errors = { errors: {} };
public lastJump: Jump = {} as Jump; public lastJump: Jump = {} as Jump;
public destroyRef = inject(DestroyRef); public destroyRef = inject(DestroyRef);
public now = new Date();
public isJumpDay = false;
constructor( constructor(
private _jumpsService: JumpsService private _jumpsService: JumpsService
@@ -50,6 +52,12 @@ export class HomeComponent implements OnInit {
this._lastjump$ = this._jumpsService.getLastJump().pipe(takeUntilDestroyed(this.destroyRef)); this._lastjump$ = this._jumpsService.getLastJump().pipe(takeUntilDestroyed(this.destroyRef));
this._lastjump = this._lastjump$.subscribe((jump) => { this._lastjump = this._lastjump$.subscribe((jump) => {
this.lastJump = jump; this.lastJump = jump;
this.isJumpDay = this._isToday(new Date(this.lastJump.date));
}); });
} }
private _isToday(someDate: Date) {
return someDate.getDate() == this.now.getDate() &&
someDate.getMonth() == this.now.getMonth() &&
someDate.getFullYear() == this.now.getFullYear()
}
} }
+4 -3
View File
@@ -1,11 +1,12 @@
export * from './auth/auth.component'; export * from './auth/auth.component';
export * from './aeronef/aeronef.component'; export * from './aeronefs/aeronefs.component';
export * from './calculator/calculator.component'; export * from './calculator/calculator.component';
export * from './canopy/canopy.component'; export * from './canopies/canopies.component';
export * from './dashboard/dashboard.component'; export * from './dashboard/dashboard.component';
export * from './dropzone/dropzone.component'; export * from './dropzones/dropzones.component';
export * from './home/home.component'; export * from './home/home.component';
export * from './jump/jump.component'; export * from './jump/jump.component';
export * from './jumps/jumps.component';
export * from './logbook/logbook.component'; export * from './logbook/logbook.component';
export * from './profile/profile.component'; export * from './profile/profile.component';
export * from './qcm/qcm.component'; export * from './qcm/qcm.component';
+280 -129
View File
@@ -1,132 +1,283 @@
<div @flyInOut> <div class="d-flex">
<div class="content"> <div class="flex-fill">
<div class="d-flex"> <h1 class="mb-1">
<div class="flex-fill"> {{title}}
<h1 class="mb-1">{{title}}</h1> <span class="fs-3 text-accent ms-2">{{ jump.categorie }} {{ jump.module }}</span>
<span [hidden]="!lastJump.numero" class="me-2 fs-6"> <span class="fs-3 text-muted ms-2">{{ jump.voile }} {{ jump.taille }}</span>
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> </h1>
</span> <span class="me-2 fs-6">
</div> Le <span class="fs-5 mx-1 text-primary">{{ jump.date | date: 'dd/MM/yyyy' }}</span>
<span class="flex-spacer"></span> à <span class="fs-5 mx-1 text-primary">{{ jump.lieu }} {{ jump.oaci }}</span>
<div class="align-self-center me-2"> @if (jump.x2data) {
<button mat-icon-button [matMenuTriggerFor]="menuAeronef" aria-label="Menu Aeronef"> depuis <span class="fs-5 mx-1 text-primary">{{ jump.aeronef }} {{ jump.imat }}</span>
<mat-icon fontIcon="more_vert"></mat-icon> }
</span>
</div>
<span class="flex-spacer"></span>
<div class="align-self-center me-4">
<button mat-button [routerLink]="['/jump', prevSlug]" aria-label="Saut précédent" color="accent">
<mat-icon fontIcon="navigate_before"></mat-icon> {{ jump.numero - 1}}
</button>
<div class="vr mx-2 fs-3 align-middle"></div>
<span class="mx-2 align-middle">{{ jump.numero }}</span>
<div class="vr mx-2 fs-3 align-middle"></div>
<button mat-button [routerLink]="['/jump', nextSlug]" aria-label="Saut suivant" color="accent">
<mat-icon fontIcon="navigate_next" iconPositionEnd></mat-icon> {{ jump.numero +1}}
</button>
</div>
<div class="align-self-center me-2">
<button mat-icon-button [matMenuTriggerFor]="menuJump" aria-label="Menu saut">
<mat-icon fontIcon="more_vert"></mat-icon>
</button>
<mat-menu #menuJump="matMenu">
@if (jump.x2data) {
<button mat-menu-item>
<mat-icon fontIcon="language" color="accent"></mat-icon>
<span class="text-accent fw-medium">Obtenir le KML</span>
</button> </button>
<mat-menu #menuAeronef="matMenu"> <mat-divider></mat-divider>
@for (menuitem of menuItems.getMenuPanel(); track menuitem) { }
<button mat-menu-item> @for (menuitem of menuItems.getMenuJump(); track menuitem) {
<mat-icon [fontIcon]="menuitem.icon"></mat-icon> <button mat-menu-item>
<span>{{ menuitem.name }}</span> <mat-icon [fontIcon]="menuitem.icon" [color]="menuitem.color"></mat-icon>
</button> <span class="text-{{menuitem.color}} fw-medium">{{ menuitem.name }}</span>
} </button>
</mat-menu> }
</div> </mat-menu>
</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-2">
@if(displayCharts) {
<div class="row">
<div class="col-xxl-6 col-xs-12">
<app-linearea-chart [headers]="seriesHeader" [names]="seriesName" [values]="seriesRow" [colors]="seriesColor"></app-linearea-chart>
</div>
<div class="col-xxl-6 col-xs-12">
<app-bars-chart [headers]="seriesHeader" [names]="seriesName" [values]="seriesRow" [colors]="seriesColor" [legend]="true"></app-bars-chart>
</div>
</div>
}
<div class="row">
<div class="col-xs-12">
<mat-divider class="mt-3 mb-0"></mat-divider>
<table class="table table-striped table-dark table-hover">
<thead>
<tr>
<th></th>
<th *ngFor='let name of seriesHeader; let i=index' class="text-end"> {{ name }} </th>
<th class="text-end">Total</th>
</tr>
</thead>
<tbody>
<tr *ngFor='let values of seriesRow; let i=index'>
<th class="{{seriesColorClass[i]}} text-end"> ● {{ seriesName[i] }} </th>
<td *ngFor='let value of values; let i=index' class="font-monospace text-end">
<span *ngIf="value; else elseBlock" class="pr-1">{{ value }}</span>
<ng-template #elseBlock><span class="text-empty pr-1">{{ value }}</span></ng-template>
</td>
<th class="{{seriesColorClass[i]}} font-monospace text-end">{{ seriesRowTotal[i] }}</th>
</tr>
</tbody>
<tfoot>
<tr>
<th class="text-end">Total</th>
<th *ngFor='let value of seriesColTotal; let i=index' class="font-monospace text-end">
<span *ngIf="value; else elseBlock" class="pr-1">{{ value }}</span>
<ng-template #elseBlock><span class="text-empty pr-1">{{ value }}</span></ng-template>
</th>
<th class="font-monospace text-end">{{ grandTotal }}</th>
</tr>
<tr>
<th class="text-end">Moyenne</th>
<th *ngFor='let value of seriesRowAvg; let i=index' class="font-monospace text-end">
<span *ngIf="value; else elseBlock" class="pr-1">{{ value | number : '1.0-1' }}</span>
<ng-template #elseBlock><span class="text-empty pr-1">{{ value | number : '1.0-1' }}</span></ng-template>
</th>
<th class="font-monospace text-end">{{ grandAvg | number : '1.0-1' }}</th>
</tr>
<tr>
<th class="text-end">Moyenne <small>3 dernières années</small></th>
<th *ngFor='let value of seriesRowAvgLastYears; let i=index' class="font-monospace text-end">
<span *ngIf="value; else elseBlock" class="pr-1">{{ value | number : '1.0-1' }}</span>
<ng-template #elseBlock><span class="text-empty pr-1">{{ value | number : '1.0-1' }}</span></ng-template>
</th>
<th class="font-monospace text-end">{{ grandAvgLastYears | number : '1.0-1' }}</th>
</tr>
</tfoot>
</table>
</div>
</div>
</mat-card-content>
</mat-card>
<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>
</div> </div>
<mat-divider class="my-3"></mat-divider>
<mat-card>
<mat-card-content>
<div class="row">
<div class="col-lg-6 col-md-12">
<dl class="dl-horizontal">
<dt class="text-light-info fw-normal">Aeronef:</dt>
<dd>{{ jump.aeronef }} {{ jump.imat }}</dd>
<dt class="text-light-info fw-normal">Voile:</dt>
<dd>{{ jump.voile }} {{ jump.taille }} ft<sup>2</sup></dd>
@if (jump.programme) {
<dt class="text-light-info fw-normal">Programme:</dt>
<dd>{{ jump.programme }}</dd>
}
</dl>
</div>
<div class="col-lg-6 col-md-12">
<dl class="dl-horizontal">
@if (!jump.x2data) {
<dt class="text-light-info fw-normal">Hauteur:</dt>
<dd>{{ jump.hauteur }} m</dd>
<dt class="text-light-info fw-normal">Déploiement:</dt>
<dd>{{ jump.deploiement }} m</dd>
}
@for (file of jump.files; track file) {
<dt class="text-light-info fw-normal">Fichier {{ file.type }}</dt>
<dd>{{ file.name }}</dd>
}
</dl>
</div>
</div>
</mat-card-content>
</mat-card>
@if (jump.x2data) {
<mat-card class="mt-3">
<mat-card-content>
<div class="row my-3">
<div class="col-sm-4 col-xs-12 text-center">
<h6>Hauteur</h6>
@if (jump.x2data.altitude.exit > 4200) {
<mat-icon class="align-middle fs-5 me-2" fontIcon="mood" color="primary"></mat-icon>
}
<span class="text-primary fw-medium fs-5">{{ jump.x2data.altitude.exit }} m</span>
</div>
<div class="col-sm-4 col-xs-12 text-center">
<h6 class="d-flex justify-content-center"><mat-icon fontIcon="timer" class="me-1 fs-5"></mat-icon> Durée de la chute</h6>
<span class="text-primary fw-medium fs-4">{{ formatDuration(jump.x2data.duration.freeFall) }}</span>
</div>
<div class="col-sm-4 col-xs-12 text-center">
<h6>Déploiement</h6>
@if (jump.x2data.altitude.deployment < 650) {
<mat-icon class="align-middle fs-5 me-2" fontIcon="warning" color="raspberry"></mat-icon>
<span class="text-raspberry fw-medium fs-5">{{ jump.x2data.altitude.deployment }} m</span>
}
@else if (jump.x2data.altitude.deployment < 750) {
<mat-icon class="align-middle fs-5 me-2" fontIcon="local_florist" color="warn"></mat-icon>
<span class="text-warn fw-medium fs-5">{{ jump.x2data.altitude.deployment }} m</span>
}
@else if (jump.x2data.altitude.deployment > 1000) {
<mat-icon class="align-middle fs-5 me-2" fontIcon="mood" color="primary"></mat-icon>
<span class="text-primary fw-medium fs-5">{{ jump.x2data.altitude.deployment }} m</span>
}
</div>
</div>
<div class="row mt-4">
<div class="{{ jump.accessoires ? 'col-sm-6 ' : '' }}col-xs-12 text-center">
<h5 class="d-flex justify-content-center mb-4"><mat-icon fontIcon="people" class="me-2"></mat-icon> Participants ({{ jump.sautants.length + 1 }})</h5>
@for (sautant of jump.sautants; track sautant) {
<span class="badge rounded-pill bg-primary mb-2 me-1 pe-3 py-1 fw-normal text-dark">
<mat-icon class="align-middle me-1" fontIcon="person"></mat-icon>
{{ sautant }}
</span>
}
<span class="badge rounded-pill bg-accent mb-2 me-1 pe-3 py-1 fw-normal text-dark">
<mat-icon class="align-middle me-1" fontIcon="person"></mat-icon>
{{ jump.author.username }}
</span>
</div>
@if (jump.accessoires) {
<div class="col-sm-6 col-xs-12 text-center">
<h5 class="d-flex justify-content-center mb-4"><mat-icon fontIcon="auto_awesome" class="me-2"></mat-icon> Accessoires</h5>
<span class="badge rounded-pill bg-navy-light mb-2 me-1 pe-3 py-1 fw-normal">
<mat-icon class="align-middle me-1" fontIcon="auto_awesome"></mat-icon>
{{ jump.accessoires }}
</span>
</div>
}
</div>
</mat-card-content>
</mat-card>
<mat-card class="mt-3">
<mat-card-content>
<div class="row my-3">
<div class="col-xs-12 text-center">
<h5 class="d-flex justify-content-center mb-3"><mat-icon fontIcon="speed" class="me-1"></mat-icon> Vitesse en chute</h5>
</div>
<div class="col-sm-6 hidden-xs text-center">
<h6>Verticale</h6>
</div>
<div class="col-sm-6 hidden-xs text-center">
<h6>Horizontale</h6>
</div>
<div class="col-sm-3 col-xs-12 text-center">
<h6>Vitesse maximale</h6>
<span class="text-primary fw-medium fs-5">{{ (jump.x2data.speed.freeFall.vertical.max / 1000 * 60 * 60) | number : '1.0-0' }} km/h</span>
</div>
<div class="col-sm-3 col-xs-12 text-center">
<h6>Vitesse moyenne</h6>
<span class="text-primary fw-medium fs-5">{{ (jump.x2data.speed.freeFall.vertical.avg / 1000 * 60 * 60) | number : '1.0-0' }} km/h</span>
</div>
<div class="col-sm-3 col-xs-12 text-center">
<h6>Vitesse maximale</h6>
<span class="text-primary fw-medium fs-5">{{ (jump.x2data.speed.freeFall.horizontal.max / 1000 * 60 * 60) | number : '1.0-0' }} km/h</span>
</div>
<div class="col-sm-3 col-xs-12 text-center">
<h6>Vitesse moyenne</h6>
<span class="text-primary fw-medium fs-5">{{ (jump.x2data.speed.freeFall.horizontal.avg / 1000 * 60 * 60) | number : '1.0-0' }} km/h</span>
</div>
</div>
</mat-card-content>
</mat-card>
<mat-card class="mt-3">
<mat-card-content>
<div class="row my-3">
<div class="col-xs-12 text-center">
<h5 class="d-flex justify-content-center mb-3"><mat-icon fontIcon="speed" class="me-1"></mat-icon> Vitesse sous voile</h5>
</div>
<div class="col-sm-6 hidden-xs text-center">
<h6>Verticale</h6>
</div>
<div class="col-sm-6 hidden-xs text-center">
<h6>Horizontale</h6>
</div>
<div class="col-sm-3 col-xs-12 text-center">
<h6>Vitesse maximale</h6>
<span class="text-primary fw-medium fs-5">{{ (jump.x2data.speed.underCanopy.vertical.max / 1000 * 60 * 60) | number : '1.0-0' }} km/h</span>
</div>
<div class="col-sm-3 col-xs-12 text-center">
<h6>Vitesse moyenne</h6>
<span class="text-primary fw-medium fs-5">{{ (jump.x2data.speed.underCanopy.vertical.avg / 1000 * 60 * 60) | number : '1.0-0' }} km/h</span>
</div>
<div class="col-sm-3 col-xs-12 text-center">
<h6>Vitesse maximale</h6>
<span class="text-primary fw-medium fs-5">{{ (jump.x2data.speed.underCanopy.horizontal.max / 1000 * 60 * 60) | number : '1.0-0' }} km/h</span>
</div>
<div class="col-sm-3 col-xs-12 text-center">
<h6>Vitesse moyenne</h6>
<span class="text-primary fw-medium fs-5">{{ (jump.x2data.speed.underCanopy.horizontal.avg / 1000 * 60 * 60) | number : '1.0-0' }} km/h</span>
</div>
</div>
</mat-card-content>
</mat-card>
<mat-card class="mt-3">
<mat-card-content>
<div class="row my-3">
<div class="col-xs-12 text-center">
<h5 class="d-flex justify-content-center mb-3"><mat-icon fontIcon="square_foot" class="me-1"></mat-icon> Finesse</h5>
</div>
<div class="col-sm-6 hidden-xs text-center">
<h6>En chute</h6>
</div>
<div class="col-sm-6 hidden-xs text-center">
<h6>Sous voile</h6>
</div>
<div class="col-sm-3 col-xs-12 text-center">
<h6>Finesse Max</h6>
<span class="text-primary fw-medium fs-5">{{ jump.x2data.glideRatio.freeFall.max | number : '1.2-2' }}</span>
</div>
<div class="col-sm-3 col-xs-12 text-center">
<h6>Finesse Moyenne</h6>
<span class="text-primary fw-medium fs-5">{{ jump.x2data.glideRatio.freeFall.avg | number : '1.2-2' }}</span>
</div>
<div class="col-sm-3 col-xs-12 text-center">
<h6>Finesse Max</h6>
<span class="text-primary fw-medium fs-5">{{ jump.x2data.glideRatio.underCanopy.max | number : '1.2-2' }}</span>
</div>
<div class="col-sm-3 col-xs-12 text-center">
<h6>Finesse Moyenne</h6>
<span class="text-primary fw-medium fs-5">{{ jump.x2data.glideRatio.underCanopy.avg | number : '1.2-2' }}</span>
</div>
</div>
</mat-card-content>
</mat-card>
<mat-card class="mt-3">
<mat-card-content>
<div class="row my-3">
<div class="col-xs-12 text-center">
<h5 class="d-flex justify-content-center mb-3"><mat-icon fontIcon="straighten" class="me-1"></mat-icon> Distance</h5>
</div>
<div class="col-sm-3 col-xs-12 text-center">
<h6>Distance au largage</h6>
<span class="text-primary fw-medium fs-5">{{ jump.x2data.distance.exitFromLandingZone| number : '1.0-0' }} m</span>
</div>
<div class="col-sm-3 col-xs-12 text-center">
<h6>Durée sous voile</h6>
<span class="text-primary fw-medium fs-5">{{ formatDuration(jump.x2data.duration.underCanopy) }}</span>
</div>
<div class="col-sm-3 col-xs-12 text-center">
<h6>Distance totale</h6>
<span class="text-primary fw-medium fs-5">{{ jump.x2data.distance.totalFlying | number : '1.0-0' }} m</span>
</div>
<div class="col-sm-3 col-xs-12 text-center">
<h6>Distance horizontale</h6>
<span class="text-primary fw-medium fs-5">{{ jump.x2data.distance.totalHorizontal | number : '1.0-0' }} m</span>
</div>
</div>
</mat-card-content>
</mat-card>
}
@else {
<mat-card class="mt-3">
<mat-card-content>
<div class="row my-3">
<div class="col-xs-12 text-center">
<h5 class="d-flex justify-content-center mb-4"><mat-icon fontIcon="people" class="me-1"></mat-icon> Participants ({{ jump.sautants.length + 1 }})</h5>
@for (sautant of jump.sautants; track sautant) {
<span class="badge rounded-pill bg-primary mb-2 me-1 pe-3 py-1 fw-normal text-dark">
<mat-icon class="align-middle me-1" fontIcon="person"></mat-icon>
{{ sautant }}
</span>
}
<span class="badge rounded-pill bg-accent mb-2 me-1 pe-3 py-1 fw-normal text-dark">
<mat-icon class="align-middle me-1" fontIcon="person"></mat-icon>
{{ jump.author.username }}
</span>
</div>
</div>
</mat-card-content>
</mat-card>
<mat-card class="mt-3">
<mat-card-content>
<div class="d-flex justify-content-center my-3 fs-5"><mat-icon fontIcon="satellite_alt" class="me-2"></mat-icon> Aucune donnée GPS</div>
</mat-card-content>
</mat-card>
}
@@ -6,10 +6,12 @@ describe('JumpComponent', () => {
let component: JumpComponent; let component: JumpComponent;
let fixture: ComponentFixture<JumpComponent>; let fixture: ComponentFixture<JumpComponent>;
beforeEach(() => { beforeEach(async () => {
TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [JumpComponent] imports: [JumpComponent]
}); })
.compileComponents();
fixture = TestBed.createComponent(JumpComponent); fixture = TestBed.createComponent(JumpComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
fixture.detectChanges(); fixture.detectChanges();
+33 -210
View File
@@ -1,248 +1,71 @@
import { trigger, state, style, animate, transition } from '@angular/animations';
import { Component, DestroyRef, inject, OnInit, OnDestroy } from '@angular/core'; import { Component, DestroyRef, inject, OnInit, OnDestroy } from '@angular/core';
import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { ActivatedRoute } from '@angular/router'; import { DatePipe, DecimalPipe } from '@angular/common';
import { CommonModule } from '@angular/common'; import { ActivatedRoute, RouterLink } 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';
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 { Observable, Subscription } from 'rxjs'; import { Observable, Subscription } from 'rxjs';
import { MenuItems } from 'src/app/components/shared'; import { ListErrorsComponent, MenuItems } from 'src/app/components/shared';
import { BarsChartComponent, CircleChartComponent, LineChartComponent, LineAreaChartComponent } from 'src/app/components/shared/helpers-chart'; import { Errors, Jump, JumpPageData } from 'src/app/core/models';
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', selector: 'app-jump',
standalone: true, standalone: true,
imports: [ imports: [
CommonModule, DatePipe, DecimalPipe, RouterLink,
MatButtonModule, MatCardModule, MatDividerModule, MatButtonModule, MatCardModule,
MatExpansionModule, MatIconModule, MatMenuModule, MatDividerModule, MatIconModule, MatMenuModule,
BarsChartComponent, CircleChartComponent, LineChartComponent, LineAreaChartComponent ListErrorsComponent
], ],
templateUrl: './jump.component.html', templateUrl: './jump.component.html',
styleUrl: './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 _data: Subscription = new Subscription(); private _data: Subscription = new Subscription();
private _jumpsByCategorie: Array<JumpByCategorie> = []; public title: string = '';
private _jumpsByModule: Array<JumpByModule> = []; public errors: Errors = { errors: {} };
private _jumpsByDate: Array<JumpByDate> = [];
public lastJump: Jump = {} as Jump;
public title: string = 'Sauts';
public subtitle: string = 'Nombre total de sauts par mois';
public displayCharts = false;
public destroyRef = inject(DestroyRef); public destroyRef = inject(DestroyRef);
public min = 0; public jump: Jump = {} as Jump;
public max = 0; public prevSlug: string = '';
public grandAvg = 0; public nextSlug: string = '';
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 _utilitiesService: UtilitiesService,
public menuItems: MenuItems public menuItems: MenuItems
) { } ) { }
ngOnInit() { ngOnInit() {
const data$: Observable<{ jumpsPageData: JumpsPageData }> = this.route.data as Observable<{ jumpsPageData: JumpsPageData }>; const data$: Observable<{ pageData: JumpPageData }> = this.route.data as Observable<{ pageData: JumpPageData }>;
this._data = data$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((data: { jumpsPageData: JumpsPageData }) => { this._data = data$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((data: { pageData: JumpPageData }) => {
const pageData: JumpsPageData = data.jumpsPageData; this.jump = data.pageData.jump;
this.lastJump = pageData.lastjump; this.prevSlug = data.pageData.prevJump?.slug;
const currentYear: number = new Date().getFullYear(); this.nextSlug = data.pageData.nextJump?.slug;
const types: Array<string> = ['FF', 'Solo', 'VR', 'Wingsuit']; this.title = `Saut n° ${this.jump.numero} `;
const verticals: Array<string> = ['Chutas', 'Head Up', 'Head Down'];
const others: Array<string> = ['Track', 'Trace'];
const modules: Array<string> = [...verticals, ...others];
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;
}); });
/*
const data$: Observable<Jump> = this.route.data as Observable<Jump>;
this._data = data$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((data: Jump) => {
this.jump = data;
console.log('jump : ', this.jump);
});
*/
} }
ngOnDestroy() { ngOnDestroy() {
this._data.unsubscribe(); this._data.unsubscribe();
} }
public getCategories(): Array<JumpByCategorie> { formatDuration(duration: number): string {
return this._jumpsByCategorie; const start = new Date(0);
start.setSeconds(duration);
return start.toISOString().substring(14, 19);
} }
public getModules(): Array<JumpByModule> { private _resetErrors(): void {
return this._jumpsByModule; this.errors = { errors: {} };
}
public getModuleColor(index: number): number {
return (this._jumpsByCategorie.length + index);
} }
} }
@@ -0,0 +1,130 @@
<div @flyInOut>
<div class="d-flex">
<div class="flex-fill">
<h1 class="mb-1">{{title}}</h1>
<span [hidden]="!lastJump.numero" class="me-2 fs-6">
Dernier saut enregistré : <span class="fs-5 mx-1 text-primary">{{ lastJump.numero }}<sup>ème</sup></span> à <span class="fs-5 mx-1 text-primary">{{ lastJump.lieu }}</span> le <span class="fs-5 mx-1 text-primary">{{ lastJump.date | date: 'dd/MM/yyyy' }}</span>
</span>
</div>
<span class="flex-spacer"></span>
<div class="align-self-center me-2">
<button mat-icon-button [matMenuTriggerFor]="menuJumps" aria-label="Menu sauts">
<mat-icon fontIcon="more_vert"></mat-icon>
</button>
<mat-menu #menuJumps="matMenu">
@for (menuitem of menuItems.getMenuPanel(); track menuitem) {
<button mat-menu-item>
<mat-icon [fontIcon]="menuitem.icon"></mat-icon>
<span>{{ menuitem.name }}</span>
</button>
}
</mat-menu>
</div>
</div>
<mat-divider class="my-3"></mat-divider>
<mat-card>
<mat-card-header>
<mat-card-title>Types de sauts</mat-card-title>
</mat-card-header>
<mat-card-content class="mt-2">
@if(displayCharts) {
<div class="row">
<div class="col-xs-12">
<app-line-chart [headers]="seriesTypesHeader" [names]="seriesTypesName" [values]="seriesTypesRowCumulated" [colors]="seriesTypesColor" [legend]="true" [hideZero]="true"></app-line-chart>
</div>
</div>
<mat-divider class="mt-2 mb-3"></mat-divider>
<div class="row">
@for (item of getCategories(); track $index) {
<div class="col-lg-2 col-sm-4 col-xs-6">
<app-circle-chart [headers]="seriesHeader" [names]="seriesName" [values]="[item.count, (lastJump.numero-item.count)]" [color]="$index" [label]="item.categorie"></app-circle-chart>
</div>
}
@for (item of getModules(); track $index) {
<div class="col-lg-2 col-sm-4 col-xs-6">
<app-circle-chart [headers]="seriesHeader" [names]="seriesName" [values]="[item.count, (lastJump.numero-item.count)]" [color]="getModuleColor($index)" [label]="item.module"></app-circle-chart>
</div>
}
</div>
<mat-divider class="my-3"></mat-divider>
<!--
<div class="row">
<div class="col-xxl-6 col-xs-12">
<app-line-chart [headers]="seriesTypesHeader" [names]="seriesTypesName" [values]="seriesTypesRow" [colors]="seriesTypesColor" [legend]="true"></app-line-chart>
</div>
<div class="col-xxl-6 col-xs-12">
<app-line-chart [headers]="seriesTypesHeader" [names]="seriesTypesName" [values]="seriesTypesRowCumulated" [colors]="seriesTypesColor" [legend]="true"></app-line-chart>
</div>
</div>
-->
}
</mat-card-content>
</mat-card>
<mat-card class="mt-3">
<mat-card-header>
<mat-card-title>{{ subtitle }}</mat-card-title>
</mat-card-header>
<mat-card-content class="mt-2">
@if(displayCharts) {
<div class="row">
<div class="col-xxl-6 col-xs-12">
<app-linearea-chart [headers]="seriesHeader" [names]="seriesName" [values]="seriesRow" [colors]="seriesColor"></app-linearea-chart>
</div>
<div class="col-xxl-6 col-xs-12">
<app-bars-chart [headers]="seriesHeader" [names]="seriesName" [values]="seriesRow" [colors]="seriesColor" [legend]="true"></app-bars-chart>
</div>
</div>
}
<div class="row">
<div class="col-xs-12">
<mat-divider class="mt-3 mb-0"></mat-divider>
<table class="table table-striped table-dark table-hover">
<thead>
<tr>
<th></th>
<th *ngFor='let name of seriesHeader; let i=index' class="text-end"> {{ name }} </th>
<th class="text-end">Total</th>
</tr>
</thead>
<tbody>
<tr *ngFor='let values of seriesRow; let i=index'>
<th class="{{seriesColorClass[i]}} text-end"> ● {{ seriesName[i] }} </th>
<td *ngFor='let value of values; let i=index' class="font-monospace text-end">
<span *ngIf="value; else elseBlock" class="pr-1">{{ value }}</span>
<ng-template #elseBlock><span class="text-empty pr-1">{{ value }}</span></ng-template>
</td>
<th class="{{seriesColorClass[i]}} font-monospace text-end">{{ seriesRowTotal[i] }}</th>
</tr>
</tbody>
<tfoot>
<tr>
<th class="text-end">Total</th>
<th *ngFor='let value of seriesColTotal; let i=index' class="font-monospace text-end">
<span *ngIf="value; else elseBlock" class="pr-1">{{ value }}</span>
<ng-template #elseBlock><span class="text-empty pr-1">{{ value }}</span></ng-template>
</th>
<th class="font-monospace text-end">{{ grandTotal }}</th>
</tr>
<tr>
<th class="text-end">Moyenne</th>
<th *ngFor='let value of seriesRowAvg; let i=index' class="font-monospace text-end">
<span *ngIf="value; else elseBlock" class="pr-1">{{ value | number : '1.0-1' }}</span>
<ng-template #elseBlock><span class="text-empty pr-1">{{ value | number : '1.0-1' }}</span></ng-template>
</th>
<th class="font-monospace text-end">{{ grandAvg | number : '1.0-1' }}</th>
</tr>
<tr>
<th class="text-end">Moyenne <small>3 dernières années</small></th>
<th *ngFor='let value of seriesRowAvgLastYears; let i=index' class="font-monospace text-end">
<span *ngIf="value; else elseBlock" class="pr-1">{{ value | number : '1.0-1' }}</span>
<ng-template #elseBlock><span class="text-empty pr-1">{{ value | number : '1.0-1' }}</span></ng-template>
</th>
<th class="font-monospace text-end">{{ grandAvgLastYears | number : '1.0-1' }}</th>
</tr>
</tfoot>
</table>
</div>
</div>
</mat-card-content>
</mat-card>
</div>
@@ -0,0 +1,21 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { JumpsComponent } from './jumps.component';
describe('JumpsComponent', () => {
let component: JumpsComponent;
let fixture: ComponentFixture<JumpsComponent>;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [JumpsComponent]
});
fixture = TestBed.createComponent(JumpsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
+249
View File
@@ -0,0 +1,249 @@
import { trigger, state, style, animate, transition } from '@angular/animations';
import { Component, DestroyRef, inject, OnInit, OnDestroy } from '@angular/core';
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { ActivatedRoute } from '@angular/router';
import { CommonModule } from '@angular/common';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatDividerModule } from '@angular/material/divider';
import { MatExpansionModule } from '@angular/material/expansion';
import { MatIconModule } from '@angular/material/icon';
import { MatMenuModule } from '@angular/material/menu';
import { Observable, Subscription } from 'rxjs';
import { MenuItems } from 'src/app/components/shared';
import { BarsChartComponent, CircleChartComponent, LineChartComponent, LineAreaChartComponent } from 'src/app/components/shared/helpers-chart';
import { UtilitiesService } from 'src/app/core/services';
import { Jump, JumpByCategorie, JumpByDate, JumpByDateByModule, JumpByModule, JumpsPageData, JumpYears } from 'src/app/core/models';
@Component({
selector: 'app-jumps',
standalone: true,
imports: [
CommonModule,
MatButtonModule, MatCardModule, MatDividerModule,
MatExpansionModule, MatIconModule, MatMenuModule,
BarsChartComponent, CircleChartComponent, LineChartComponent, LineAreaChartComponent
],
templateUrl: './jumps.component.html',
styleUrl: './jumps.component.scss',
animations: [
trigger('flyInOut', [
state('in', style({ transform: 'translateX(0)' })),
transition('void => *', [
style({ transform: 'translateX(-100%)' }),
animate(200)
]),
transition('* => void', [
style({ transform: 'translateX(100%)' }),
animate(200)
])
])
]
})
export class JumpsComponent implements OnInit, OnDestroy {
private _data: Subscription = new Subscription();
private _jumpsByCategorie: Array<JumpByCategorie> = [];
private _jumpsByModule: Array<JumpByModule> = [];
private _jumpsByDate: Array<JumpByDate> = [];
public lastJump: Jump = {} as Jump;
public title: string = 'Sauts';
public subtitle: string = 'Nombre total de sauts par mois';
public displayCharts = false;
public destroyRef = inject(DestroyRef);
public min = 0;
public max = 0;
public grandAvg = 0;
public grandAvgLastYears = 0;
public grandTotal = 0;
public seriesHeader: string[] = [];
public seriesName: string[] = [];
public seriesRow: Array<Array<number>> = [];
public seriesColor: {
backgroundColor: string[],
borderColor: string[]
} = {
backgroundColor: this._utilitiesService.getSeriesColors(0.8, 'all'),
borderColor: this._utilitiesService.getSeriesColors(1, 'all')
};
public seriesTypesHeader: string[] = [];
public seriesTypesName: string[] = [];
public seriesTypesRow: Array<Array<number>> = [];
public seriesTypesRowCumulated: Array<Array<number>> = [];
public seriesTypesColor: {
backgroundColor: string[],
borderColor: string[]
} = {
backgroundColor: this._utilitiesService.getSeriesColors(0.8, 'pastels'),
borderColor: this._utilitiesService.getSeriesColors(1, 'pastels')
};
public seriesColorClass: string[] = this._utilitiesService.getChartColors();
public seriesRowTotal: number[] = [];
public seriesRowAvg: number[] = [];
public seriesRowAvgLastYears: number[] = [];
public seriesColTotal: number[] = [];
public seriesColTotalClosed: number[] = [];
public seriesColTotalLastYears: number[] = [];
constructor(
private route: ActivatedRoute,
private _utilitiesService: UtilitiesService,
public menuItems: MenuItems
) { }
ngOnInit() {
const data$: Observable<{ pageData: JumpsPageData }> = this.route.data as Observable<{ pageData: JumpsPageData }>;
this._data = data$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((data: { pageData: JumpsPageData }) => {
const pageData: JumpsPageData = data.pageData;
this.lastJump = pageData.lastjump;
const currentYear: number = new Date().getFullYear();
const types: Array<string> = ['FF', 'Solo', 'VR', 'Wingsuit'];
const verticals: Array<string> = ['Chutas', 'Head Up', 'Head Down'];
const others: Array<string> = ['Track', 'Trace'];
const modules: Array<string> = [...verticals, ...others];
const minoration = 2; // -2 pour ne pas compter la première année
this.min = pageData.jumpsByYears[0].year;
this.max = pageData.jumpsByYears[(pageData.jumpsByYears.length-1)].year;
this.seriesHeader = this._utilitiesService.getMonthsList();
const values: Array<number> = this.seriesHeader.map(() => 0);
this.seriesColTotal = [...values];
this.seriesColTotalClosed = [...values];
this.seriesColTotalLastYears = [...values];
this.seriesName = pageData.jumpsByYears.map((row: JumpYears) => {
this.seriesRow.push([...values]);
return row.year.toString();
});
this._jumpsByDate = pageData.jumpsByDate.map((row: JumpByDate) => {
this.seriesRow[(row.year-this.min)][(row.month-1)] = row.count;
this.seriesColTotal[(row.month-1)] += row.count;
if (row.year < currentYear) {
this.seriesColTotalClosed[(row.month-1)] += row.count;
}
if (row.year >= (currentYear-3) && row.year < currentYear) {
this.seriesColTotalLastYears[(row.month-1)] += row.count;
}
return row;
});
this.seriesRow.forEach((row: number[]) => {
const total = row.reduce((partialSum, a) => partialSum + a, 0);
this.seriesRowTotal.push(total);
});
this.seriesColTotalClosed.forEach((row: number) => {
const value: number = (row/(this.seriesName.length-minoration));
this.seriesRowAvg.push(parseInt(value.toFixed(0)));
});
this.seriesColTotalLastYears.forEach((row: number) => {
const value: number = (row/3); // Les 3 dernières années
this.seriesRowAvgLastYears.push(parseInt(value.toFixed(0)));
});
this.grandAvg = this.seriesRowAvg.reduce((partialSum, accumulated) => partialSum + accumulated, 0);
this.grandAvgLastYears = this.seriesRowAvgLastYears.reduce((partialSum, accumulated) => partialSum + accumulated, 0);
this.grandTotal = this.seriesColTotal.reduce((partialSum, accumulated) => partialSum + accumulated, 0);
pageData.jumpsByCategory.forEach((row: JumpByCategorie) => {
if (types.indexOf(row.categorie) !== -1) {
this._jumpsByCategorie.push(row);
}
});
//const focus: Array<string> = ['FF', 'Solo']; // ['FF']
let verticalCount:number = 0;
let otherCount:number = 0;
pageData.jumpsByModule.forEach((row: JumpByModule) => {
//if (focus.indexOf(row.categorie) !== -1 && modules.indexOf(row.module) !== -1) {
if (modules.indexOf(row.module) !== -1) {
if (verticals.indexOf(row.module) !== -1) {
verticalCount += row.count;
} else {
otherCount += row.count;
}
}
});
if (verticalCount > 0) {
const row:JumpByModule = {
categorie: 'FF',
module: 'Vertical',
count: verticalCount
}
this._jumpsByModule.push(row);
}
if (otherCount > 0) {
const row:JumpByModule = {
categorie: 'FF',
module: 'Track/Trace',
count: otherCount
}
this._jumpsByModule.push(row);
}
for (let year = 0; year < pageData.jumpsByYears.length; year++) {
for (let index = 0; index < 12; index++) {
const month: string = ((index+1) < 10) ? `0${(index+1)}` : `${(index+1)}`;
this.seriesTypesHeader.push(`${month}-${pageData.jumpsByYears[year].year}`);
}
}
const emptyValues: Array<number> = this.seriesTypesHeader.map(() => 0);
this.seriesTypesName = types;
this.seriesTypesName.forEach(() => {
this.seriesTypesRow.push([...emptyValues]);
});
pageData.jumpsByDateByModule.forEach((row: JumpByDateByModule) => {
const indexOf: number = this.seriesTypesName.indexOf(row.categorie);
if (indexOf !== -1) {
this.seriesTypesRow[indexOf][(((row.year-this.min)*12)+row.month-1)] += row.count;
}
});
this.seriesTypesRowCumulated = this.seriesTypesRow.map((row: Array<number>) => {
let accumulated: number = 0;
return row.map((value: number) => {
accumulated += value;
return accumulated;
});
});
this.seriesTypesHeader = this.seriesTypesHeader.slice((pageData.jumpsByDateByModule[0].month+1), -(pageData.jumpsByDateByModule[(pageData.jumpsByDateByModule.length-1)].month));
this.seriesTypesRow = this.seriesTypesRow.map((row: Array<number>) => {
return row.slice((pageData.jumpsByDateByModule[0].month+1), -(pageData.jumpsByDateByModule[(pageData.jumpsByDateByModule.length-1)].month));
});
this.seriesTypesRowCumulated = this.seriesTypesRowCumulated.map((row: Array<number>) => {
return row.slice((pageData.jumpsByDateByModule[0].month+1), -(pageData.jumpsByDateByModule[(pageData.jumpsByDateByModule.length-1)].month));
});
/*
console.log('this.min', this.min);
console.log('this.max', this.max);
console.log('this.lastJump', this.lastJump);
console.log('this.seriesHeader', this.seriesHeader);
console.log('this.seriesName', this.seriesName);
console.log('this.seriesRow', this.seriesRow);
console.log('this.seriesRowTotal', this.seriesRowTotal);
console.log('this.seriesRowAvg', this.seriesRowAvg);
console.log('this.seriesRowAvgLastYears', this.seriesRowAvgLastYears);
console.log('this._jumpsByCategorie', this._jumpsByCategorie);
console.log('this._jumpsByModule', this._jumpsByModule);
console.log('this.seriesTypesHeader', this.seriesTypesHeader);
console.log('this.seriesTypesName', this.seriesTypesName);
console.log('this.seriesTypesRow', this.seriesTypesRow);
console.log('this.seriesTypesRowCumulated', this.seriesTypesRowCumulated);
*/
this.displayCharts = true;
});
}
ngOnDestroy() {
this._data.unsubscribe();
}
public getCategories(): Array<JumpByCategorie> {
return this._jumpsByCategorie;
}
public getModules(): Array<JumpByModule> {
return this._jumpsByModule;
}
public getModuleColor(index: number): number {
return (this._jumpsByCategorie.length + index);
}
}
@@ -245,7 +245,7 @@
} }
<span class="badge rounded-pill bg-accent mb-2 me-1 pe-3 py-1 fw-normal text-dark"> <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> <mat-icon class="align-middle pointer me-1" fontIcon="person"></mat-icon>
ju_solide {{ lastJump.author.username }}
</span> </span>
</div> </div>
</div> </div>
@@ -180,7 +180,7 @@
} }
<span class="badge rounded-pill bg-accent mb-2 me-1 pe-3 py-1 fw-normal text-dark"> <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> <mat-icon class="align-middle pointer me-1" fontIcon="person"></mat-icon>
ju_solide {{ jump.author.username }}
</span> </span>
</div> </div>
</div> </div>
@@ -1,69 +1,214 @@
<h2 mat-dialog-title class="bg-cyan border-bottom border-light-subtle d-flex mb-3 pt-3 pb-2"> <h2 mat-dialog-title class="bg-navy-light border-bottom border-light-subtle d-flex mb-3 pt-3 pb-2">
Saut n°{{jump.numero}}<span class="flex-spacer"></span> 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">
<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="d-flex border-bottom border-light-subtle text-light-info pb-2 mb-3">
<div class="row mb-3"> <div class="flex-fill">
<div class="col-lg-6 col-md-12"> <h5>Le {{ jump.date | date: 'dd/MM/yyyy' }} à {{ jump.lieu }} {{ jump.zone ? jump.zone : jump.oaci }} :</h5>
<dl class="dl-horizontal"> </div>
<dt class="text-cyan fw-normal">Lieu:</dt> <span class="flex-spacer"></span>
<dd>{{jump.lieu}}</dd> <div class="align-self-center me-2">
<dt class="text-cyan fw-normal">Oaci:</dt> <span class="text-accent fs-5">{{ jump.categorie }}</span>
<dd>{{jump.oaci}}</dd> @if (jump.module) {
<dt class="text-cyan fw-normal">Aeronef:</dt> <span class="fs-5 mx-2">/</span>
<dd>{{jump.aeronef}}</dd> <span class="text-muted fs-5">{{ jump.module }}</span>
<dt class="text-cyan fw-normal">Imat:</dt> }
<dd>{{jump.imat}}</dd> </div>
<dt class="text-cyan fw-normal">Voile:</dt> </div>
<dd>{{jump.voile}}</dd> <div class="row">
<dt class="text-cyan fw-normal">Taille:</dt> <div class="col-lg-6 col-md-12">
<dd>{{jump.taille}} ft<sup>2</sup></dd> <dl class="dl-horizontal">
<dt class="text-cyan fw-normal">Participants:</dt> <dt class="text-light-info fw-normal">Aeronef:</dt>
<dd>{{jump.participants}}</dd> <dd>{{ jump.aeronef }} {{ jump.imat }}</dd>
<dt class="text-cyan fw-normal">Sautants:</dt> <dt class="text-light-info fw-normal">Voile:</dt>
<dd> <dd>{{ jump.voile }} {{ jump.taille }} ft<sup>2</sup></dd>
@for (sautant of jump.sautants; track sautant) { @if (jump.programme) {
<span class="badge rounded-pill bg-primary mb-2 me-1 pe-3 py-1 fw-normal text-dark"> <dt class="text-light-info fw-normal">Programme:</dt>
<mat-icon class="align-middle me-1" fontIcon="person"></mat-icon> <dd>{{ jump.programme }}</dd>
{{ sautant }} }
</span> </dl>
} </div>
<span class="badge rounded-pill bg-accent mb-2 me-1 pe-3 py-1 fw-normal text-dark"> <div class="col-lg-6 col-md-12">
<mat-icon class="align-middle me-1" fontIcon="person"></mat-icon> <dl class="dl-horizontal">
ju_solide @if (!jump.x2data) {
</span> <dt class="text-light-info fw-normal">Hauteur:</dt>
</dd> <dd>{{ jump.hauteur }} m</dd>
</dl> <dt class="text-light-info fw-normal">Déploiement:</dt>
</div> <dd>{{ jump.deploiement }} m</dd>
<div class="col-lg-6 col-md-12"> }
<dl class="dl-horizontal"> @for (file of jump.files; track file) {
<dt class="text-cyan fw-normal">Hauteur:</dt> <dt class="text-light-info fw-normal">Fichier {{ file.type }}</dt>
<dd>{{jump.hauteur}} m</dd> <dd>{{ file.name }}</dd>
<dt class="text-cyan fw-normal">Deploiement:</dt> }
<dd>{{jump.deploiement}} m</dd> </dl>
<dt class="text-cyan fw-normal">Categorie:</dt> </div>
<dd>{{jump.categorie}}</dd> </div>
<dt class="text-cyan fw-normal">Module:</dt> <mat-divider></mat-divider>
<dd>{{jump.module}}</dd> @if (jump.x2data) {
<dt class="text-cyan fw-normal">Accessoires:</dt> <div class="row my-3">
<dd>{{jump.accessoires}}</dd> <div class="col-sm-4 col-xs-12 text-center">
<dt class="text-cyan fw-normal">Zone:</dt> <h6>Hauteur</h6>
<dd>{{jump.zone}}</dd> <span class="text-primary fw-medium fs-5">{{ jump.x2data.altitude.exit }} m</span>
<dt class="text-cyan fw-normal">Dossier media:</dt> </div>
<dd>{{jump.dossier}}</dd> <div class="col-sm-4 col-xs-12 text-center">
<dt class="text-cyan fw-normal">Video:</dt> <h6 class="d-flex justify-content-center"><mat-icon fontIcon="timer" class="me-1 fs-5"></mat-icon> Durée de la chute</h6>
<dd>{{jump.video}}</dd> <span class="text-primary fw-medium fs-5">{{formatDuration(jump.x2data.duration.freeFall)}}</span>
<dt class="text-cyan fw-normal">Programme:</dt> </div>
<dd>{{jump.programme}}</dd> <div class="col-sm-4 col-xs-12 text-center">
</dl> <h6>Déploiement</h6>
</div> <span class="text-primary fw-medium fs-5">{{ jump.x2data.altitude.deployment }} m</span>
</div>
</div>
<div class="row my-4">
<div class="{{ jump.accessoires ? 'col-sm-6 ' : '' }}col-xs-12 text-center">
<h5 class="d-flex justify-content-center mb-4"><mat-icon fontIcon="people" class="me-1"></mat-icon> Participants ({{ jump.sautants.length + 1 }})</h5>
@for (sautant of jump.sautants; track sautant) {
<span class="badge rounded-pill bg-primary mb-2 me-1 pe-3 py-1 fw-normal text-dark">
<mat-icon class="align-middle me-1" fontIcon="person"></mat-icon>
{{ sautant }}
</span>
}
<span class="badge rounded-pill bg-accent mb-2 me-1 pe-3 py-1 fw-normal text-dark">
<mat-icon class="align-middle me-1" fontIcon="person"></mat-icon>
{{ jump.author.username }}
</span>
</div>
@if (jump.accessoires) {
<div class="col-sm-6 col-xs-12 text-center">
<h5 class="d-flex justify-content-center mb-4"><mat-icon fontIcon="auto_awesome" class="me-1"></mat-icon> Accessoires</h5>
<span class="badge rounded-pill bg-navy-light mb-2 me-1 pe-3 py-1 fw-normal">
<mat-icon class="align-middle me-1" fontIcon="auto_awesome"></mat-icon>
{{ jump.accessoires }}
</span>
</div>
}
</div>
<div class="row my-3">
<div class="col-xs-12 text-center">
<h5 class="d-flex justify-content-center mb-3"><mat-icon fontIcon="speed" class="me-1"></mat-icon> Vitesse en chute</h5>
</div>
<div class="col-sm-3 col-xs-12 text-center">
<h6>Max Verticale</h6>
<span class="text-primary fw-medium fs-5">{{ (jump.x2data.speed.freeFall.vertical.max / 1000 * 60 * 60) | number : '1.0-0' }} km/h</span>
</div>
<div class="col-sm-3 col-xs-12 text-center">
<h6>Max Horizontale</h6>
<span class="text-primary fw-medium fs-5">{{ (jump.x2data.speed.freeFall.horizontal.max / 1000 * 60 * 60) | number : '1.0-0' }} km/h</span>
</div>
<div class="col-sm-3 col-xs-12 text-center">
<h6>Moyenne Verticale</h6>
<span class="text-primary fw-medium fs-5">{{ (jump.x2data.speed.freeFall.vertical.avg / 1000 * 60 * 60) | number : '1.0-0' }} km/h</span>
</div>
<div class="col-sm-3 col-xs-12 text-center">
<h6>Moyenne Horizontale</h6>
<span class="text-primary fw-medium fs-5">{{ (jump.x2data.speed.freeFall.horizontal.avg / 1000 * 60 * 60) | number : '1.0-0' }} km/h</span>
</div>
</div>
<mat-divider></mat-divider>
<div class="row mt-4 mb-3">
<div class="col-xs-12 text-center">
<h5 class="d-flex justify-content-center mb-3"><mat-icon fontIcon="speed" class="me-1"></mat-icon> Vitesse sous voile</h5>
</div>
<div class="col-sm-3 col-xs-12 text-center">
<h6>Max Verticale</h6>
<span class="text-primary fw-medium fs-5">{{ (jump.x2data.speed.underCanopy.vertical.max / 1000 * 60 * 60) | number : '1.0-0' }} km/h</span>
</div>
<div class="col-sm-3 col-xs-12 text-center">
<h6>Max Horizontale</h6>
<span class="text-primary fw-medium fs-5">{{ (jump.x2data.speed.underCanopy.horizontal.max / 1000 * 60 * 60) | number : '1.0-0' }} km/h</span>
</div>
<div class="col-sm-3 col-xs-12 text-center">
<h6>Moyenne Verticale</h6>
<span class="text-primary fw-medium fs-5">{{ (jump.x2data.speed.underCanopy.vertical.avg / 1000 * 60 * 60) | number : '1.0-0' }} km/h</span>
</div>
<div class="col-sm-3 col-xs-12 text-center">
<h6>Moyenne Horizontale</h6>
<span class="text-primary fw-medium fs-5">{{ (jump.x2data.speed.underCanopy.horizontal.avg / 1000 * 60 * 60) | number : '1.0-0' }} km/h</span>
</div>
</div>
<mat-divider></mat-divider>
<div class="row mt-3">
<div class="col-xs-12 text-center">
<h5 class="d-flex justify-content-center mb-3"><mat-icon fontIcon="square_foot" class="me-1"></mat-icon> Finesse</h5>
</div>
<div class="col-sm-6 col-xs-12 text-center">
<h6>Finesse en chute</h6>
</div>
<div class="col-sm-6 col-xs-12 text-center">
<h6>Finesse sous voile</h6>
</div>
</div>
<div class="row mb-3">
<div class="col-sm-3 col-xs-12 text-center">
<h6>Finesse Max</h6>
<span class="text-primary fw-medium fs-5">{{ jump.x2data.glideRatio.freeFall.max | number : '1.2-2' }}</span>
</div>
<div class="col-sm-3 col-xs-12 text-center">
<h6>Finesse Moyenne</h6>
<span class="text-primary fw-medium fs-5">{{ jump.x2data.glideRatio.freeFall.avg | number : '1.2-2' }}</span>
</div>
<div class="col-sm-3 col-xs-12 text-center">
<h6>Finesse Max</h6>
<span class="text-primary fw-medium fs-5">{{ jump.x2data.glideRatio.underCanopy.max | number : '1.2-2' }}</span>
</div>
<div class="col-sm-3 col-xs-12 text-center">
<h6>Finesse Moyenne</h6>
<span class="text-primary fw-medium fs-5">{{ jump.x2data.glideRatio.underCanopy.avg | number : '1.2-2' }}</span>
</div>
</div>
<mat-divider></mat-divider>
<div class="row my-3">
<div class="col-xs-12 text-center">
<h5 class="d-flex justify-content-center mb-3"><mat-icon fontIcon="straighten" class="me-1"></mat-icon> Distance</h5>
</div>
<div class="col-sm-3 col-xs-12 text-center">
<h6>Distance au largage</h6>
<span class="text-primary fw-medium fs-5">{{ jump.x2data.distance.exitFromLandingZone| number : '1.0-0' }} m</span>
</div>
<div class="col-sm-3 col-xs-12 text-center">
<h6 class="d-flex justify-content-center"><mat-icon fontIcon="timer" class="me-1 fs-5"></mat-icon> Durée sous voile</h6>
<span class="text-primary fw-medium fs-5">{{formatDuration(jump.x2data.duration.underCanopy)}}</span>
</div>
<div class="col-sm-3 col-xs-12 text-center">
<h6>Distance totale</h6>
<span class="text-primary fw-medium fs-5">{{ jump.x2data.distance.totalFlying | number : '1.0-0' }} m</span>
</div>
<div class="col-sm-3 col-xs-12 text-center">
<h6>Distance horizontale</h6>
<span class="text-primary fw-medium fs-5">{{ jump.x2data.distance.totalHorizontal | number : '1.0-0' }} m</span>
</div>
</div>
<mat-divider></mat-divider>
}
@else {
<div class="row my-4">
<div class="col-xs-12 text-center">
<h5 class="d-flex justify-content-center mb-4"><mat-icon fontIcon="people" class="me-1"></mat-icon> Participants ({{ jump.sautants.length + 1 }})</h5>
@for (sautant of jump.sautants; track sautant) {
<span class="badge rounded-pill bg-primary mb-2 me-1 pe-3 py-1 fw-normal text-dark">
<mat-icon class="align-middle me-1" fontIcon="person"></mat-icon>
{{ sautant }}
</span>
}
<span class="badge rounded-pill bg-accent mb-2 me-1 pe-3 py-1 fw-normal text-dark">
<mat-icon class="align-middle me-1" fontIcon="person"></mat-icon>
{{ jump.author.username }}
</span>
</div>
</div>
<mat-divider></mat-divider>
<div class="d-flex justify-content-center my-3 fs-5"><mat-icon fontIcon="satellite_alt" class="me-2"></mat-icon> Aucune donnée GPS</div>
<mat-divider></mat-divider>
}
</div> </div>
<mat-divider></mat-divider>
</div>
</mat-dialog-content> </mat-dialog-content>
<mat-dialog-actions> <mat-dialog-actions>
<span class="flex-spacer"></span> <span class="flex-spacer"></span>
<button mat-button (click)="closeDialog()" class="me-1">Fermer</button> <button mat-button (click)="closeDialog()" class="me-1">Fermer</button>
</mat-dialog-actions> </mat-dialog-actions>
@@ -1,5 +1,5 @@
import { Component, Inject } from '@angular/core'; import { Component, Inject } from '@angular/core';
import { DatePipe } from '@angular/common'; import { DatePipe, DecimalPipe } 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';
@@ -12,11 +12,9 @@ import { Jump } from 'src/app/core/models';
templateUrl: 'jump-view.dialog.html', templateUrl: 'jump-view.dialog.html',
standalone: true, standalone: true,
imports: [ imports: [
DatePipe, DatePipe, DecimalPipe,
MatButtonModule, MatButtonModule, MatDialogModule,
MatDialogModule, MatDividerModule, MatIconModule
MatDividerModule,
MatIconModule
] ]
}) })
export class JumpViewDialogComponent { export class JumpViewDialogComponent {
@@ -30,4 +28,10 @@ export class JumpViewDialogComponent {
// close the dialog without result // close the dialog without result
this.dialogRef.close(); this.dialogRef.close();
} }
formatDuration(duration: number): string {
const start = new Date(0);
start.setSeconds(duration);
return start.toISOString().substring(14, 19);
}
} }
@@ -1,107 +1,105 @@
<div @flyInOut> <div @flyInOut>
<div class="content"> <div class="d-flex">
<div class="d-flex"> <div class="flex-fill">
<div class="flex-fill"> <h1 class="mb-1">{{title}}</h1>
<h1 class="mb-1">{{title}}</h1> <span [hidden]="!lastJump.numero" class="me-2 fs-6">
<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>
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>
</span> </div>
</div> <span class="flex-spacer"></span>
<span class="flex-spacer"></span> <div class="align-self-center me-2">
<div class="align-self-center me-2"> <button mat-icon-button (click)="openAddDialog()" aria-label="Ajouter">
<button mat-icon-button (click)="openAddDialog()" aria-label="Ajouter"> <mat-icon fontIcon="add_notes"></mat-icon>
</button>
<button mat-icon-button [matMenuTriggerFor]="menuJump" aria-label="Menu Jump">
<mat-icon fontIcon="more_vert"></mat-icon>
</button>
<mat-menu #menuJump="matMenu">
<button mat-menu-item (click)="openAddDialog()">
<mat-icon fontIcon="add_notes"></mat-icon> <mat-icon fontIcon="add_notes"></mat-icon>
<span>Ajouter un saut</span>
</button> </button>
<button mat-icon-button [matMenuTriggerFor]="menuJump" aria-label="Menu Jump"> @for (menuitem of menuItems.getMenuLogbook(); track menuitem) {
<mat-icon fontIcon="more_vert"></mat-icon> @if (menuitem.type === 'link') {
</button> <button mat-menu-item [routerLink]="['/', menuitem.state]">
<mat-menu #menuJump="matMenu"> <mat-icon [fontIcon]="menuitem.icon"></mat-icon>
<button mat-menu-item (click)="openAddDialog()"> <span>{{ menuitem.name }}</span>
<mat-icon fontIcon="add_notes"></mat-icon>
<span>Ajouter un saut</span>
</button> </button>
@for (menuitem of menuItems.getMenuLogbook(); track menuitem) {
@if (menuitem.type === 'link') {
<button mat-menu-item [routerLink]="['/', menuitem.state]">
<mat-icon [fontIcon]="menuitem.icon"></mat-icon>
<span>{{ menuitem.name }}</span>
</button>
}
} }
</mat-menu> }
</div> </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>
<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>
@@ -12,14 +12,20 @@
</div> </div>
</div> </div>
</div> </div>
<div class="profile-page content"> <div class="profile-page">
<h1>{{title}}</h1> <div class="d-flex">
<mat-divider></mat-divider> <div class="flex-fill">
<h1 class="mb-1">{{title}}</h1>
</div>
<span class="flex-spacer"></span>
<div class="align-self-center me-2 text-end"></div>
</div>
<mat-divider class="my-3"></mat-divider>
<app-list-errors [errors]="errors"></app-list-errors> <app-list-errors [errors]="errors"></app-list-errors>
<mat-card class="mt-4 bg-light text-black scaled"> <mat-card class="mt-4 bg-light text-black scaled">
<mat-card-content> <mat-card-content>
<div class="text-center"> <div class="text-center">
<a href="index.html" class="d-block p-4"><img height="72" width="90" src="assets/images/vector/upt_logo.png" alt="Logo UPT"></a> <a href="/" class="d-block p-4"><img height="72" width="90" src="assets/images/vector/upt_logo.png" alt="Logo UPT"></a>
<mat-divider class="border-primary w-50 mx-auto"></mat-divider> <mat-divider class="border-primary w-50 mx-auto"></mat-divider>
<h1 class="mt-3 mb-4"><span class="pt-1 pb-2 px-2">Vector 3 - <small class="text-muted">V306 Micron</small></span></h1> <h1 class="mt-3 mb-4"><span class="pt-1 pb-2 px-2">Vector 3 - <small class="text-muted">V306 Micron</small></span></h1>
<mat-divider class="border-primary w-50 mx-auto"></mat-divider> <mat-divider class="border-primary w-50 mx-auto"></mat-divider>
@@ -4,6 +4,7 @@
.banner { .banner {
background-color: var.$sidebar-footer; background-color: var.$sidebar-footer;
padding: 1.6rem 0 1.3rem 0; padding: 1.6rem 0 1.3rem 0;
margin: -1.5rem -1.5rem 1.5rem -1.5rem;
.user-img { .user-img {
width: var.$user-img-size; width: var.$user-img-size;
height: var.$user-img-size; height: var.$user-img-size;
+52 -31
View File
@@ -1,32 +1,41 @@
<div class="content"> <div class="d-flex">
<div class="d-flex"> <div class="flex-fill">
<div class="flex-fill"> <h1 class="mb-1">
<h1 class="mb-1"> {{ title }}
{{ title }} <span class="ms-2 text-muted fs-4">{{ questionsCount }} {{ questionsCount > 1 ? 'questions' : 'question' }}</span>
<span class="ms-2 text-muted fs-4">{{ questionsCount }} {{ questionsCount > 1 ? 'questions' : 'question' }}</span> </h1>
</h1> <span class="me-2 fs-6">{{ subtitle }}</span>
<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> </div>
<mat-divider class="my-3"></mat-divider> <span class="flex-spacer"></span>
<app-list-errors [errors]="errors"></app-list-errors> <div class="align-self-center me-2">
<mat-card [hidden]="!questions.length"> <button mat-icon-button [matMenuTriggerFor]="menuAeronef" aria-label="Menu Aeronef">
<mat-card-content class="mt-4"> <mat-icon fontIcon="more_vert"></mat-icon>
<form [formGroup]="qcmForm"> </button>
<mat-menu #menuAeronef="matMenu">
@for (menuitem of menuItems.getMenuPanel(); track menuitem) {
<button mat-menu-item>
<mat-icon [fontIcon]="menuitem.icon"></mat-icon>
<span>{{ menuitem.name }}</span>
</button>
}
</mat-menu>
</div>
</div>
<mat-divider class="my-3"></mat-divider>
<app-list-errors [errors]="errors"></app-list-errors>
@if (questions.controls.length) {
<mat-card [hidden]="!questions.controls.length">
<div class="mx-3 my-5">
<mat-progress-bar [mode]="loading ? 'query' : 'determinate'" [value]="loading ? 0 : (totalAnswer/questionsCount*100)" color="accent"></mat-progress-bar>
<mat-progress-bar class="mt-0" [mode]="loading ? 'query' : 'determinate'" [value]="loading ? 0 : ((timeProgress/timeMax)*100)" [color]="timeProgressColor"></mat-progress-bar>
<ol class="squareItems clearfix">
@for (answer of answers; track $index) {
<li [class]="answer">{{ ($index + 1) }}</li>
}
</ol>
</div>
<mat-card-content>
<form [formGroup]="qcmForm" (ngSubmit)="onSubmit()">
@for (category of qcm.categories; track category; let index = $index) { @for (category of qcm.categories; track category; let index = $index) {
<mat-accordion> <mat-accordion>
<mat-expansion-panel class="mb-3" [expanded]="expandedIndex === index" (opened)="setExpandedIndex(index)"> <mat-expansion-panel class="mb-3" [expanded]="expandedIndex === index" (opened)="setExpandedIndex(index)">
@@ -43,9 +52,9 @@
@for (question of category.questions; track question; let index = $index) { @for (question of category.questions; track question; let index = $index) {
<div class="mb-3" [formGroupName]="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> <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()"> <mat-radio-group formControlName="choice" id="radio-group-{{ category.num }}-{{ question.num }}" class="d-flex flex-column" (change)="onChange(category, question, $event.value)">
@for (choice of question.choices; track choice) { @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-button [value]="choice.index" [color]="choice.correct ? 'primary' : 'warn'">{{ choice.libelle }}</mat-radio-button>
} }
</mat-radio-group> </mat-radio-group>
</div> </div>
@@ -56,5 +65,17 @@
} }
</form> </form>
</mat-card-content> </mat-card-content>
<!--
<form [formGroup]="qcmForm">
<div class="mb-3 ms-4" formArrayName="questions">
@for (question of questions.controls; track question; let index = $index) {
<div class="mb-3" [formGroupName]="index">
<label class="fs-5 fw-medium mb-2" for="radio-group-{{ index }}">{{ index }} - Libellé</label>
<input formControlName="choice" type="text"/>
</div>
}
</div>
</form>
-->
</mat-card> </mat-card>
</div> }
+37
View File
@@ -0,0 +1,37 @@
/* QCM */
@use 'variable' as var;
ol.squareItems {
padding: 0;
margin: 1rem 0;
li {
color: var.$primary;
background-color: var.$sidenav-content; //var.$dark;
background-image: none;
border: 1px solid var.$dark; //var.$extra-muted;
cursor: pointer;
float: left;
list-style-type: none;
margin: 0.17em;
padding: 0.34em 0;
text-align: center;
width: 33px;
height: 33px;
&.active {
border-color: var.$info;
}
&.valid {
color: var.$sidenav-content;
border-color: var.$success;
background-color: var.$primary;
}
&.defer {
border-color: var.$info-dark;
background-color: var.$navy-light;
}
&.error {
border-color: var.$purple-light;
background-color: var.$secondary;
}
}
}
+80 -19
View File
@@ -1,6 +1,6 @@
import { Component, DestroyRef, inject, OnInit, OnDestroy } from '@angular/core'; import { Component, DestroyRef, inject, OnInit, OnDestroy } from '@angular/core';
import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { FormControl, FormGroup, FormBuilder, FormArray, FormsModule, ReactiveFormsModule } from '@angular/forms'; import { /*FormControl,*/ FormGroup, FormBuilder, FormArray, FormsModule, ReactiveFormsModule } from '@angular/forms';
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } 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';
@@ -8,12 +8,15 @@ 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 { MatProgressBarModule } from '@angular/material/progress-bar';
import { MatRadioModule } from '@angular/material/radio'; import { MatRadioModule } from '@angular/material/radio';
import { Observable, Subscription } from 'rxjs'; import { MatSelectModule } from '@angular/material/select';
import { interval, Observable, Subscription } from 'rxjs';
import { take } from 'rxjs/operators';
import { MenuItems } from 'src/app/components/shared'; import { MenuItems } from 'src/app/components/shared';
import { ListErrorsComponent, } from 'src/app/components/shared'; import { ListErrorsComponent } from 'src/app/components/shared';
import { Errors, Qcm, User } from 'src/app/core/models'; import { Errors, Qcm, QcmCategory, QcmQuestion, QcmQuestionState, User } from 'src/app/core/models';
import { QcmService, UserService } from 'src/app/core/services'; import { QcmService, UserService } from 'src/app/core/services';
import data from 'src/qcm-bpa.json'; import data from 'src/qcm-bpa.json';
@@ -23,7 +26,8 @@ import data from 'src/qcm-bpa.json';
imports: [ imports: [
FormsModule, ReactiveFormsModule, FormsModule, ReactiveFormsModule,
MatButtonModule, MatCardModule, MatDividerModule, MatExpansionModule, MatButtonModule, MatCardModule, MatDividerModule, MatExpansionModule,
MatIconModule, MatMenuModule, MatRadioModule, MatIconModule, MatMenuModule, MatProgressBarModule, MatRadioModule,
MatSelectModule,
ListErrorsComponent ListErrorsComponent
], ],
templateUrl: './qcm.component.html', templateUrl: './qcm.component.html',
@@ -32,6 +36,8 @@ import data from 'src/qcm-bpa.json';
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();
private _timer: Subscription = new Subscription();
private _refresh = 1000;
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;
@@ -41,7 +47,13 @@ export class QcmComponent implements OnInit, OnDestroy {
public destroyRef = inject(DestroyRef); public destroyRef = inject(DestroyRef);
public expandedIndex = 0; public expandedIndex = 0;
public questionsCount = 0; public questionsCount = 0;
public answers: Array<QcmQuestionState> = [];
public qcmForm!: FormGroup; public qcmForm!: FormGroup;
public loading = true;
public totalAnswer = 0;
public timeProgress = 0;
public timeProgressColor = 'primary';
public timeMax = (60*45);
constructor( constructor(
private route: ActivatedRoute, private route: ActivatedRoute,
@@ -56,15 +68,16 @@ export class QcmComponent implements OnInit, OnDestroy {
} }
get questions(): FormArray { get questions(): FormArray {
return <FormArray>this.qcmForm.controls['questions']; return this.qcmForm.controls['questions'] as FormArray;
} }
ngOnInit() { ngOnInit() {
//this.importQuestions(); //this._importQuestions();
//this.importChoices(); //this._importChoices();
// Retreive the prefetched QCM // Retreive the prefetched QCM
const data$: Observable<{ qcm: Qcm }> = this.route.data as Observable<{ qcm: 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._data = data$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((data: { qcm: Qcm }) => {
this.questionsCount = 0;
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';
@@ -77,13 +90,7 @@ export class QcmComponent implements OnInit, OnDestroy {
this.qcm.categories.forEach(category => { this.qcm.categories.forEach(category => {
this.questionsCount += category.questions.length; this.questionsCount += category.questions.length;
category.questions.forEach(question => { category.questions.forEach(question => {
this.questions.push( this.addQuestion(question);
this.fb.group({
id: new FormControl({ value: question.num, disabled: false }),
choice: ['']
//choice: new FormControl({ value: '', disabled: false })
})
);
}); });
}); });
}); });
@@ -93,6 +100,7 @@ export class QcmComponent implements OnInit, OnDestroy {
this.currentUser = userData; this.currentUser = userData;
this.canModify = userData.role === 'Admin'; this.canModify = userData.role === 'Admin';
}); });
this.loading = false;
} }
ngOnDestroy() { ngOnDestroy() {
@@ -100,15 +108,64 @@ export class QcmComponent implements OnInit, OnDestroy {
this._currentUser.unsubscribe(); this._currentUser.unsubscribe();
} }
private _starTimer(): void {
this._timer = interval(1000)
.pipe(take(this.timeMax))
.subscribe(() => {
this.timeProgress++;
if (this.timeProgress > (this.timeMax/100*70) && this.timeProgress <= (this.timeMax/100*90)) {
this.timeProgressColor = 'warning';
} else if (this.timeProgress > (this.timeMax/100*90) && this.timeProgress <= (this.timeMax/100*98)) {
this.timeProgressColor = 'danger';
} else if (this.timeProgress > (this.timeMax/100*98)) {
this.timeProgressColor = 'purple';
}
if (this.timeProgress >= this.timeMax) {
this.timeProgressColor = 'accent';
this._timer.unsubscribe();
}
});
}
addQuestion(question: QcmQuestion) {
const item = this.fb.group({
id: [question.num],
choice: ['']
});
this.answers.push(QcmQuestionState.UNANSWERED);
this.questions.push(item);
}
setExpandedIndex(index: number) { setExpandedIndex(index: number) {
this.expandedIndex = index; this.expandedIndex = index;
} }
onChange() { onChange(category: QcmCategory, question: QcmQuestion, value: unknown) {
//this.isDisabled = true; //console.log('onChange', category.num, question.num, value);
if (this.timeProgress === 0) {
this._starTimer();
}
this.qcm.categories[(category.num-1)].questions[(question.num-1)].choices.forEach(choice => {
if (choice.index === value) {
if (choice.correct) {
this.answers[(question.num-1)] = QcmQuestionState.VALID;
console.log(`Réponse à la question ${question.num} de la catégorie ${category.num} : correcte`);
} else {
this.answers[(question.num-1)] = QcmQuestionState.ERROR;
console.log(`Réponse à la question ${question.num} de la catégorie ${category.num} : incorrecte`);
}
}
});
this.totalAnswer++;
this.questions.controls[(question.num-1)].disable();
//console.log(this.questions.controls[(question.num-1)].disabled);
} }
importChoices() { onSubmit() {
console.log('onSubmit');
}
private _importChoices() {
try { try {
data.categories.forEach((category) => { data.categories.forEach((category) => {
category.questions.forEach(question => { category.questions.forEach(question => {
@@ -132,7 +189,7 @@ export class QcmComponent implements OnInit, OnDestroy {
} }
} }
importQuestions() { private _importQuestions() {
try { try {
data.categories.forEach((category) => { data.categories.forEach((category) => {
//Object.assign(this.jump, category); //Object.assign(this.jump, category);
@@ -154,4 +211,8 @@ export class QcmComponent implements OnInit, OnDestroy {
console.error(error); console.error(error);
} }
} }
private _resetErrors(): void {
this.errors = { errors: {} };
}
} }
@@ -12,7 +12,7 @@
</div> </div>
</div> </div>
</div> </div>
<div class="credentials-page content"> <div class="credentials-page">
<h1>{{title}}</h1> <h1>{{title}}</h1>
<mat-divider></mat-divider> <mat-divider></mat-divider>
<app-list-errors [errors]="errors"></app-list-errors> <app-list-errors [errors]="errors"></app-list-errors>
@@ -4,6 +4,7 @@
.banner { .banner {
background-color: var.$sidebar-footer; background-color: var.$sidebar-footer;
padding: 1.6rem 0 1.3rem 0; padding: 1.6rem 0 1.3rem 0;
margin: -1.5rem -1.5rem 1.5rem -1.5rem;
.user-img { .user-img {
width: var.$user-img-size; width: var.$user-img-size;
height: var.$user-img-size; height: var.$user-img-size;
@@ -12,7 +12,7 @@
</div> </div>
</div> </div>
</div> </div>
<div class="settings-page content"> <div class="settings-page">
<h1>{{title}}</h1> <h1>{{title}}</h1>
<mat-divider></mat-divider> <mat-divider></mat-divider>
<app-list-errors [errors]="errors"></app-list-errors> <app-list-errors [errors]="errors"></app-list-errors>
@@ -4,6 +4,7 @@
.banner { .banner {
background-color: var.$sidebar-footer; background-color: var.$sidebar-footer;
padding: 1.6rem 0 1.3rem 0; padding: 1.6rem 0 1.3rem 0;
margin: -1.5rem -1.5rem 1.5rem -1.5rem;
/* /*
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;
@@ -1,9 +1,4 @@
import { import { Directive, AfterContentChecked } from '@angular/core';
Directive,
OnInit,
AfterViewInit,
AfterContentChecked
} from '@angular/core';
import { Router, NavigationEnd } from '@angular/router'; import { Router, NavigationEnd } from '@angular/router';
import { AccordionLinkDirective } from './accordionlink.directive'; import { AccordionLinkDirective } from './accordionlink.directive';
@@ -51,7 +46,7 @@ export class AccordionDirective implements AfterContentChecked {
ngAfterContentChecked(): void { ngAfterContentChecked(): void {
this.router.events this.router.events
.pipe(filter(event => event instanceof NavigationEnd)) .pipe(filter(event => event instanceof NavigationEnd))
.subscribe(e => this.checkOpenLinks()); .subscribe(() => this.checkOpenLinks());
} }
constructor(private router: Router) { constructor(private router: Router) {
@@ -14,7 +14,7 @@ export class AccordionAnchorDirective {
} }
@HostListener('click', ['$event']) @HostListener('click', ['$event'])
onClick(e: any) { onClick() {
this.navlink.toggle(); this.navlink.toggle();
} }
} }
@@ -15,7 +15,7 @@ import { AccordionDirective } from './accordion.directive';
}) })
export class AccordionLinkDirective implements OnInit, OnDestroy { export class AccordionLinkDirective implements OnInit, OnDestroy {
@Input() @Input()
public group: any; public group!: string;
@HostBinding('class.selected') @HostBinding('class.selected')
@Input() @Input()
@@ -37,15 +37,15 @@ export class AccordionLinkDirective implements OnInit, OnDestroy {
this.nav = nav; this.nav = nav;
} }
ngOnInit(): any { ngOnInit() {
this.nav.addLink(this); this.nav.addLink(this);
} }
ngOnDestroy(): any { ngOnDestroy(){
this.nav.removeGroup(this); this.nav.removeGroup(this);
} }
toggle(): any { toggle() {
this.selected = !this.selected; this.selected = !this.selected;
} }
} }
@@ -0,0 +1,25 @@
import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing';
import { AeronefsPieComponent } from './aeronefs-pie.component';
describe('AeronefsPieComponent', () => {
let component: AeronefsPieComponent;
let fixture: ComponentFixture<AeronefsPieComponent>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
imports: [AeronefsPieComponent]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(AeronefsPieComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -16,16 +16,16 @@ import { UtilitiesService, AeronefsService } from 'src/app/core/services';
import { AeronefByImat, AeronefByYear } from 'src/app/core/models'; import { AeronefByImat, AeronefByYear } from 'src/app/core/models';
@Component({ @Component({
selector: 'app-aeronefs', selector: 'app-aeronefs-pie',
standalone: true, standalone: true,
imports: [ imports: [
MatBadgeModule, MatButtonModule, MatCardModule, MatDividerModule, MatBadgeModule, MatButtonModule, MatCardModule, MatDividerModule,
MatExpansionModule, MatIconModule, MatMenuModule, MatExpansionModule, MatIconModule, MatMenuModule,
HistoryTableComponent, PieChartComponent HistoryTableComponent, PieChartComponent
], ],
templateUrl: './aeronefs.component.html' templateUrl: './aeronefs-pie.component.html'
}) })
export class AeronefsComponent implements OnInit, OnDestroy { export class AeronefsPieComponent implements OnInit, OnDestroy {
private _aeronefByImat: Subscription = new Subscription(); private _aeronefByImat: Subscription = new Subscription();
private _aeronefByYear: Subscription = new Subscription(); private _aeronefByYear: Subscription = new Subscription();
private _aeronefsByImat!: Array<AeronefByImat>; private _aeronefsByImat!: Array<AeronefByImat>;
@@ -1,20 +1,20 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { DropZonesComponent } from './drop-zones.component'; import { CanopiesModelsComponent } from './canopies-models.component';
describe('DropZonesComponent', () => { describe('CanopiesModelsComponent', () => {
let component: DropZonesComponent; let component: CanopiesModelsComponent;
let fixture: ComponentFixture<DropZonesComponent>; let fixture: ComponentFixture<CanopiesModelsComponent>;
beforeEach(async(() => { beforeEach(async(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [DropZonesComponent] imports: [CanopiesModelsComponent]
}) })
.compileComponents(); .compileComponents();
})); }));
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(DropZonesComponent); fixture = TestBed.createComponent(CanopiesModelsComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
fixture.detectChanges(); fixture.detectChanges();
}); });
@@ -15,16 +15,16 @@ import { UtilitiesService, CanopiesService } from 'src/app/core/services';
import { CanopyModelBySize, CanopyModelByYear } from 'src/app/core/models'; import { CanopyModelBySize, CanopyModelByYear } from 'src/app/core/models';
@Component({ @Component({
selector: 'app-canopy-models', selector: 'app-canopies-models',
standalone: true, standalone: true,
imports: [ imports: [
MatButtonModule, MatCardModule, MatDividerModule, MatButtonModule, MatCardModule, MatDividerModule,
MatExpansionModule, MatIconModule, MatMenuModule, MatExpansionModule, MatIconModule, MatMenuModule,
PieChartComponent, HistoryTableComponent PieChartComponent, HistoryTableComponent
], ],
templateUrl: './canopy-models.component.html' templateUrl: './canopies-models.component.html'
}) })
export class CanopyModelsComponent implements OnInit, OnDestroy { export class CanopiesModelsComponent implements OnInit, OnDestroy {
private _canopyBySize: Subscription = new Subscription(); private _canopyBySize: Subscription = new Subscription();
private _canopyByYear: Subscription = new Subscription(); private _canopyByYear: Subscription = new Subscription();
private _canopiesBySize!: Array<CanopyModelBySize>; private _canopiesBySize!: Array<CanopyModelBySize>;
@@ -1,20 +1,20 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { DropZonesBarComponent } from './drop-zones-bar.component'; import { CanopiesSizesComponent } from './canopies-sizes.component';
describe('DropZonesBarComponent', () => { describe('CanopiesSizesComponent', () => {
let component: DropZonesBarComponent; let component: CanopiesSizesComponent;
let fixture: ComponentFixture<DropZonesBarComponent>; let fixture: ComponentFixture<CanopiesSizesComponent>;
beforeEach(async(() => { beforeEach(async(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [DropZonesBarComponent] imports: [CanopiesSizesComponent]
}) })
.compileComponents(); .compileComponents();
})); }));
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(DropZonesBarComponent); fixture = TestBed.createComponent(CanopiesSizesComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
fixture.detectChanges(); fixture.detectChanges();
}); });
@@ -16,16 +16,16 @@ import { UtilitiesService, CanopiesService } from 'src/app/core/services';
import { CanopyBySize, CanopyByYear } from 'src/app/core/models'; import { CanopyBySize, CanopyByYear } from 'src/app/core/models';
@Component({ @Component({
selector: 'app-canopy-sizes', selector: 'app-canopies-sizes',
standalone: true, standalone: true,
imports: [ imports: [
MatButtonModule, MatCardModule, MatDividerModule, MatButtonModule, MatCardModule, MatDividerModule,
MatExpansionModule, MatIconModule, MatMenuModule, MatExpansionModule, MatIconModule, MatMenuModule,
PieChartComponent, HistoryTableComponent PieChartComponent, HistoryTableComponent
], ],
templateUrl: './canopy-sizes.component.html' templateUrl: './canopies-sizes.component.html'
}) })
export class CanopySizesComponent implements OnInit, OnDestroy { export class CanopiesSizesComponent implements OnInit, OnDestroy {
private _canopyBySize: Subscription = new Subscription(); private _canopyBySize: Subscription = new Subscription();
private _canopyByYear: Subscription = new Subscription(); private _canopyByYear: Subscription = new Subscription();
private _canopiesBySize!: Array<CanopyBySize>; private _canopiesBySize!: Array<CanopyBySize>;
@@ -1,20 +1,20 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { CanopyModelsComponent } from './canopy-models.component'; import { DropzonesBarComponent } from './dropzones-bar.component';
describe('CanopyModelsComponent', () => { describe('DropzonesBarComponent', () => {
let component: CanopyModelsComponent; let component: DropzonesBarComponent;
let fixture: ComponentFixture<CanopyModelsComponent>; let fixture: ComponentFixture<DropzonesBarComponent>;
beforeEach(async(() => { beforeEach(async(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [CanopyModelsComponent] imports: [DropzonesBarComponent]
}) })
.compileComponents(); .compileComponents();
})); }));
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(CanopyModelsComponent); fixture = TestBed.createComponent(DropzonesBarComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
fixture.detectChanges(); fixture.detectChanges();
}); });
@@ -17,7 +17,7 @@ import { UtilitiesService, DropZonesService } from 'src/app/core/services';
import { DropZoneByOaci, DropZoneByYear } from 'src/app/core/models'; import { DropZoneByOaci, DropZoneByYear } from 'src/app/core/models';
@Component({ @Component({
selector: 'app-drop-zones-bar', selector: 'app-dropzones-bar',
standalone: true, standalone: true,
imports: [ imports: [
MatButtonModule, MatCardModule, MatDividerModule, MatButtonModule, MatCardModule, MatDividerModule,
@@ -25,9 +25,9 @@ import { DropZoneByOaci, DropZoneByYear } from 'src/app/core/models';
BaseChartDirective, ChartistModule, BaseChartDirective, ChartistModule,
HistoryTableComponent HistoryTableComponent
], ],
templateUrl: './drop-zones-bar.component.html' templateUrl: './dropzones-bar.component.html'
}) })
export class DropZonesBarComponent implements OnInit, OnDestroy { export class DropzonesBarComponent implements OnInit, OnDestroy {
private _dropzoneByOaci: Subscription = new Subscription(); private _dropzoneByOaci: Subscription = new Subscription();
private _dropzoneByYear: Subscription = new Subscription(); private _dropzoneByYear: Subscription = new Subscription();
private _dropzonesByOaci!: Array<DropZoneByOaci>; private _dropzonesByOaci!: Array<DropZoneByOaci>;
@@ -1,20 +1,20 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { CanopySizesComponent } from './canopy-sizes.component'; import { DropzonesPieComponent } from './dropzones-pie.component';
describe('CanopySizesComponent', () => { describe('DropzonesPieComponent', () => {
let component: CanopySizesComponent; let component: DropzonesPieComponent;
let fixture: ComponentFixture<CanopySizesComponent>; let fixture: ComponentFixture<DropzonesPieComponent>;
beforeEach(async(() => { beforeEach(async(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [CanopySizesComponent] imports: [DropzonesPieComponent]
}) })
.compileComponents(); .compileComponents();
})); }));
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(CanopySizesComponent); fixture = TestBed.createComponent(DropzonesPieComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
fixture.detectChanges(); fixture.detectChanges();
}); });
@@ -16,16 +16,16 @@ import { UtilitiesService, DropZonesService } from 'src/app/core/services';
import { DropZoneByOaci, DropZoneByYear } from 'src/app/core/models'; import { DropZoneByOaci, DropZoneByYear } from 'src/app/core/models';
@Component({ @Component({
selector: 'app-drop-zones', selector: 'app-dropzones-pie',
standalone: true, standalone: true,
imports: [ imports: [
MatButtonModule, MatCardModule, MatDividerModule, MatButtonModule, MatCardModule, MatDividerModule,
MatExpansionModule, MatIconModule, MatMenuModule, MatExpansionModule, MatIconModule, MatMenuModule,
PieChartComponent, HistoryTableComponent PieChartComponent, HistoryTableComponent
], ],
templateUrl: './drop-zones.component.html' templateUrl: './dropzones-pie.component.html'
}) })
export class DropZonesComponent implements OnInit, OnDestroy { export class DropzonesPieComponent implements OnInit, OnDestroy {
private _dropzoneByOaci: Subscription = new Subscription(); private _dropzoneByOaci: Subscription = new Subscription();
private _dropzoneByYear: Subscription = new Subscription(); private _dropzoneByYear: Subscription = new Subscription();
private _dropzonesByOaci!: Array<DropZoneByOaci>; private _dropzonesByOaci!: Array<DropZoneByOaci>;
@@ -1,7 +1,7 @@
export * from './aeronefs/aeronefs.component'; export * from './aeronefs-pie/aeronefs-pie.component';
export * from './aeronefs-bar/aeronefs-bar.component'; export * from './aeronefs-bar/aeronefs-bar.component';
export * from './canopy-models/canopy-models.component'; export * from './canopies-models/canopies-models.component';
export * from './canopy-sizes/canopy-sizes.component'; export * from './canopies-sizes/canopies-sizes.component';
export * from './drop-zones/drop-zones.component'; export * from './dropzones-pie/dropzones-pie.component';
export * from './drop-zones-bar/drop-zones-bar.component'; export * from './dropzones-bar/dropzones-bar.component';
export * from './jumps-by-month/jumps-by-month.component'; export * from './jumps-by-month/jumps-by-month.component';
@@ -74,6 +74,7 @@ export class JumpsByMonthComponent implements OnInit, OnDestroy {
} }
private _loadJumpByDate() { private _loadJumpByDate() {
const minoration = 2; // -2 pour ne pas compter la première année
this.seriesHeader = this._utilitiesService.getMonthsList(); this.seriesHeader = this._utilitiesService.getMonthsList();
const values: Array<number> = this.seriesHeader.map(() => 0); const values: Array<number> = this.seriesHeader.map(() => 0);
this.seriesColTotal = [...values]; this.seriesColTotal = [...values];
@@ -111,7 +112,7 @@ export class JumpsByMonthComponent implements OnInit, OnDestroy {
this.seriesRowTotal.push(total); this.seriesRowTotal.push(total);
}); });
this.seriesColTotalClosed.forEach((row: number) => { this.seriesColTotalClosed.forEach((row: number) => {
const value: number = (row/(this.seriesName.length-2)); // -2 pour ne pas compter la première année const value: number = (row/(this.seriesName.length-minoration));
this.seriesRowAvg.push(parseInt(value.toFixed(0))); this.seriesRowAvg.push(parseInt(value.toFixed(0)));
}); });
this.seriesColTotalLastYears.forEach((row: number) => { this.seriesColTotalLastYears.forEach((row: number) => {
@@ -3,7 +3,7 @@
<canvas baseChart class="w-100 h-auto" <canvas baseChart class="w-100 h-auto"
[data]="chartConfig.barChartData" [data]="chartConfig.barChartData"
[options]="chartConfig.barChartOptions" [options]="chartConfig.barChartOptions"
[plugins]="chartConfig.barChartPlugins" [plugins]="chartConfig.barChartPlugins!"
[legend]="chartConfig.barChartLegend" [legend]="chartConfig.barChartLegend"
[type]="'bar'"> [type]="'bar'">
</canvas> </canvas>
@@ -39,7 +39,7 @@ export class BarChartComponent implements OnChanges {
} }
private _loadBarChart(): void { private _loadBarChart(): void {
const data: ChartDataset<'bar'> = { const dataset: ChartDataset<'bar'> = {
data: [...this.values], data: [...this.values],
label: this.label, label: this.label,
backgroundColor: this.colors.backgroundColor, backgroundColor: this.colors.backgroundColor,
@@ -47,7 +47,7 @@ export class BarChartComponent implements OnChanges {
borderWidth: 1 borderWidth: 1
}; };
this.chartConfig.barChartData.labels = [...this.names]; this.chartConfig.barChartData.labels = [...this.names];
this.chartConfig.barChartData.datasets!.push(data); this.chartConfig.barChartData.datasets!.push(dataset);
/*this.chartConfig.barChartOptions!.scales = { /*this.chartConfig.barChartOptions!.scales = {
x: {display: false}, x: {display: false},
y: {display: true} y: {display: true}
@@ -3,7 +3,7 @@
<canvas baseChart <canvas baseChart
[data]="chartConfig.barChartData" [data]="chartConfig.barChartData"
[options]="chartConfig.barChartOptions" [options]="chartConfig.barChartOptions"
[plugins]="chartConfig.barChartPlugins" [plugins]="chartConfig.barChartPlugins!"
[legend]="chartConfig.barChartLegend" [legend]="chartConfig.barChartLegend"
[type]="'bar'"> [type]="'bar'">
</canvas> </canvas>
@@ -39,7 +39,7 @@ export class BarHorizontalChartComponent implements OnChanges {
} }
private _loadBarChart(): void { private _loadBarChart(): void {
const data: ChartDataset<'bar'> = { const dataset: ChartDataset<'bar'> = {
data: [...this.values], data: [...this.values],
label: this.label, label: this.label,
backgroundColor: this.colors.backgroundColor, backgroundColor: this.colors.backgroundColor,
@@ -47,7 +47,7 @@ export class BarHorizontalChartComponent implements OnChanges {
borderWidth: 1 borderWidth: 1
}; };
this.chartConfig.barChartData.labels = [...this.names]; this.chartConfig.barChartData.labels = [...this.names];
this.chartConfig.barChartData.datasets!.push(data); this.chartConfig.barChartData.datasets!.push(dataset);
this.chartConfig.barChartLegend = this.legend; this.chartConfig.barChartLegend = this.legend;
this.displayCharts = true; this.displayCharts = true;
} }
@@ -3,7 +3,7 @@
<canvas baseChart class="w-100 h-auto" <canvas baseChart class="w-100 h-auto"
[data]="chartConfig.barChartData" [data]="chartConfig.barChartData"
[options]="chartConfig.barChartOptions" [options]="chartConfig.barChartOptions"
[plugins]="chartConfig.barChartPlugins" [plugins]="chartConfig.barChartPlugins!"
[legend]="chartConfig.barChartLegend" [legend]="chartConfig.barChartLegend"
[type]="'bar'"> [type]="'bar'">
</canvas> </canvas>
@@ -34,7 +34,7 @@ export class BarsChartComponent implements OnChanges, AfterContentChecked {
@Input() legend: boolean = false; @Input() legend: boolean = false;
ngOnChanges() { ngOnChanges() {
//this._loadBarChart(); this._loadBarChart();
} }
ngAfterContentChecked() { ngAfterContentChecked() {
@@ -63,7 +63,7 @@ export class BarsChartComponent implements OnChanges, AfterContentChecked {
} }
}; };
this.names.forEach((label: string, index: number) => { this.names.forEach((label: string, index: number) => {
const data: ChartDataset<'bar'> = { const dataset: ChartDataset<'bar'> = {
data: [...this.values[index]], data: [...this.values[index]],
label: label, label: label,
backgroundColor: this.colors.backgroundColor[index], backgroundColor: this.colors.backgroundColor[index],
@@ -71,7 +71,7 @@ export class BarsChartComponent implements OnChanges, AfterContentChecked {
borderWidth: 1, borderWidth: 1,
//stack: 'Stack 0' //stack: 'Stack 0'
}; };
this.chartConfig.barChartData.datasets!.push(data); this.chartConfig.barChartData.datasets!.push(dataset);
}); });
this.chartConfig.barChartLegend = this.legend; this.chartConfig.barChartLegend = this.legend;
this.displayCharts = true; this.displayCharts = true;
@@ -50,7 +50,7 @@ export class CircleChartComponent implements OnChanges {
} }
/*private _loadCircleChart(): void { /*private _loadCircleChart(): void {
const data: ChartDataset<'circle'> = { const dataset: ChartDataset<'circle'> = {
data: [...this.values], data: [...this.values],
label: 'Nombre total de sauts', label: 'Nombre total de sauts',
backgroundColor: this._utilitiesService.getSeriesColors(0.8), backgroundColor: this._utilitiesService.getSeriesColors(0.8),
@@ -58,7 +58,7 @@ export class CircleChartComponent implements OnChanges {
borderWidth: 1 borderWidth: 1
}; };
this.chartConfig.circleChartLabels = [...this.names]; this.chartConfig.circleChartLabels = [...this.names];
this.chartConfig.circleChartDatasets!.push(data); this.chartConfig.circleChartDatasets!.push(dataset);
this.displayCharts = true; this.displayCharts = true;
}*/ }*/
} }
@@ -1,5 +1,5 @@
import { Component, Input, OnChanges } from '@angular/core'; import { Component, Input, OnChanges } from '@angular/core';
import { ChartDataset } from 'chart.js'; import { ChartDataset, Point } from 'chart.js';
import { BaseChartDirective } from 'ng2-charts'; import { BaseChartDirective } from 'ng2-charts';
import { UtilitiesService } from 'src/app/core/services'; import { UtilitiesService } from 'src/app/core/services';
@@ -32,6 +32,7 @@ export class LineChartComponent implements OnChanges {
borderColor: this._utilitiesService.getSeriesColors(1) borderColor: this._utilitiesService.getSeriesColors(1)
}; };
@Input() legend: boolean = false; @Input() legend: boolean = false;
@Input() hideZero: boolean = false;
ngOnChanges() { ngOnChanges() {
this._loadLineChart(); this._loadLineChart();
@@ -40,8 +41,14 @@ export class LineChartComponent implements OnChanges {
private _loadLineChart(): void { private _loadLineChart(): void {
this.chartConfig.lineChartData.labels = [...this.headers]; this.chartConfig.lineChartData.labels = [...this.headers];
this.names.forEach((label: string, index: number) => { this.names.forEach((label: string, index: number) => {
const data: ChartDataset<'line'> = { let data: Array<(number | Point | null)> = [];
data: [...this.values[index]], if (this.hideZero) {
data = [...this.values[index].map(value => value === 0 ? null : value)];
} else {
data = [...this.values[index]];
}
const dataset: ChartDataset<'line'> = {
data: data,
label: label, label: label,
backgroundColor: this.colors.backgroundColor[index], backgroundColor: this.colors.backgroundColor[index],
borderColor: this.colors.borderColor[index], borderColor: this.colors.borderColor[index],
@@ -49,7 +56,7 @@ export class LineChartComponent implements OnChanges {
cubicInterpolationMode: 'monotone', cubicInterpolationMode: 'monotone',
tension: 0 tension: 0
}; };
this.chartConfig.lineChartData.datasets!.push(data); this.chartConfig.lineChartData.datasets!.push(dataset);
}); });
this.chartConfig.lineChartLegend = this.legend; this.chartConfig.lineChartLegend = this.legend;
this.displayCharts = true; this.displayCharts = true;
@@ -44,7 +44,7 @@ export class LineAreaChartComponent implements OnChanges {
if (index === 0) { if (index === 0) {
fill = true; fill = true;
} }
const data: ChartDataset<'line'> = { const dataset: ChartDataset<'line'> = {
data: [...this.values[index]], data: [...this.values[index]],
label: label, label: label,
backgroundColor: this.colors.backgroundColor[index], backgroundColor: this.colors.backgroundColor[index],
@@ -55,7 +55,7 @@ export class LineAreaChartComponent implements OnChanges {
cubicInterpolationMode: 'monotone', cubicInterpolationMode: 'monotone',
tension: 0 tension: 0
}; };
this.chartConfig.lineChartData.datasets!.push(data); this.chartConfig.lineChartData.datasets!.push(dataset);
}); });
this.chartConfig.lineChartLegend = this.legend; this.chartConfig.lineChartLegend = this.legend;
this.displayCharts = true; this.displayCharts = true;
@@ -49,7 +49,7 @@ export class PieChartComponent implements OnChanges {
} }
/*private _loadDoughnutChart(): void { /*private _loadDoughnutChart(): void {
const data: ChartDataset<'doughnut'> = { const dataset: ChartDataset<'doughnut'> = {
data: [...this.values], data: [...this.values],
label: 'Nombre total de sauts', label: 'Nombre total de sauts',
backgroundColor: this._utilitiesService.getSeriesColors(0.8), backgroundColor: this._utilitiesService.getSeriesColors(0.8),
@@ -57,7 +57,7 @@ export class PieChartComponent implements OnChanges {
borderWidth: 1 borderWidth: 1
}; };
this.chartConfig.doughnutChartLabels = [...this.names]; this.chartConfig.doughnutChartLabels = [...this.names];
this.chartConfig.doughnutChartDatasets!.push(data); this.chartConfig.doughnutChartDatasets!.push(dataset);
this.displayCharts = true; this.displayCharts = true;
}*/ }*/
} }
@@ -1,4 +1,4 @@
import { Component, Input, OnChanges, AfterContentChecked } from '@angular/core'; import { Component, Input, AfterContentChecked } from '@angular/core';
import { NgIf, NgFor } from '@angular/common'; import { NgIf, NgFor } from '@angular/common';
@Component({ @Component({
@@ -10,7 +10,7 @@ import { NgIf, NgFor } from '@angular/common';
templateUrl: './history-table.component.html', templateUrl: './history-table.component.html',
styleUrls: [] styleUrls: []
}) })
export class HistoryTableComponent implements OnChanges, AfterContentChecked { export class HistoryTableComponent implements AfterContentChecked {
public seriesColTotal: number[] = []; public seriesColTotal: number[] = [];
public grandTotal: number = 0; public grandTotal: number = 0;
@@ -22,10 +22,10 @@ export class HistoryTableComponent implements OnChanges, AfterContentChecked {
@Input() rows: Array<Array<number>> = []; @Input() rows: Array<Array<number>> = [];
@Input() colors: string[] = []; @Input() colors: string[] = [];
ngOnChanges() { /*ngOnChanges() {
//this._loadHistoryTable(); //this._loadHistoryTable();
//console.log('onChanges'); //console.log('onChanges');
} }*/
ngAfterContentChecked() { ngAfterContentChecked() {
if (this.rows !== undefined && this.rows.length) { if (this.rows !== undefined && this.rows.length) {
@@ -1,11 +1,10 @@
import { Component, OnInit, OnDestroy, Input, computed, Signal, WritableSignal } from '@angular/core'; import { Component, OnInit, OnDestroy, Input } from '@angular/core';
import { NgClass, DatePipe } from '@angular/common'; import { NgClass, DatePipe } from '@angular/common';
import { RouterLink } from '@angular/router'; import { RouterLink } from '@angular/router';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon'; import { MatIconModule } from '@angular/material/icon';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { Observable, Subscription } from 'rxjs'; import { Observable, Subscription } from 'rxjs';
import { take } from 'rxjs/operators';
import { Errors, Jump, JumpList, JumpListConfig } from 'src/app/core/models'; import { Errors, Jump, JumpList, JumpListConfig } from 'src/app/core/models';
import { JumpsService } from 'src/app/core/services'; import { JumpsService } from 'src/app/core/services';
@@ -39,7 +39,7 @@ export class JumpPreviewComponent implements OnDestroy {
deleteJump() { deleteJump() {
this.isDeleting = true; this.isDeleting = true;
const jump$: Observable<any> = this.jumpsService.destroy(this.jump.slug); const jump$: Observable<boolean> = this.jumpsService.destroy(this.jump.slug);
this._jump = jump$.subscribe({ this._jump = jump$.subscribe({
next: () => { next: () => {
this.router.navigateByUrl('/'); this.router.navigateByUrl('/');
@@ -12,7 +12,7 @@
</button> </button>
<mat-spinner [diameter]="32" [hidden]="!loading" color="accent" class="align-middle ms-2"></mat-spinner> <mat-spinner [diameter]="32" [hidden]="!loading" color="accent" class="align-middle ms-2"></mat-spinner>
<span [hidden]="jumpsCount || loading" class="ms-2 fst-italic">Aucun saut à afficher pour le moment.</span> <span [hidden]="jumpsCount || loading" class="ms-2 fst-italic">Aucun saut à afficher pour le moment.</span>
<span [hidden]="!loading" class="ms-2 fst-italic">Actualisation des sauts en cours</span> <span [hidden]="!loading" class="ms-2 fst-italic">Chargement des sauts en cours</span>
</form> </form>
</div> </div>
<div class="col-md-6 col-xs-12 px-0"> <div class="col-md-6 col-xs-12 px-0">
@@ -224,7 +224,7 @@
</ng-container> </ng-container>
</td> </td>
</ng-container> --> </ng-container> -->
<!-- Video Column --> <!-- Video Column
<ng-container matColumnDef="video"> <ng-container matColumnDef="video">
<th class="pl-3" mat-header-cell *matHeaderCellDef> Video </th> <th class="pl-3" mat-header-cell *matHeaderCellDef> Video </th>
<td class="pl-3 text-nowrap" mat-cell *matCellDef="let element"> <td class="pl-3 text-nowrap" mat-cell *matCellDef="let element">
@@ -236,6 +236,41 @@
</ngx-skeleton-loader> </ngx-skeleton-loader>
} }
</td> </td>
</ng-container> -->
<!-- Files Column -->
<ng-container matColumnDef="files">
<th class="pl-3" mat-header-cell *matHeaderCellDef> Fichiers </th>
<td class="pl-3 text-nowrap" mat-cell *matCellDef="let element">
@if (element.files) {
@for (file of element.files; track $index) {
@switch (file.type) {
@case ('video') {
<button mat-button (click)="openViewDialog(element)" color="navy-light">
<mat-icon fontIcon="videocam me-2"></mat-icon> {{ file.type }}
</button>
}
@case ('image') {
<button mat-button (click)="openViewDialog(element)" color="navy-light">
<mat-icon fontIcon="image me-2"></mat-icon> {{ file.type }}
</button>
}
@case ('kml') {
<button mat-button (click)="openViewDialog(element)" color="navy-light">
<mat-icon fontIcon="language me-2"></mat-icon> {{ file.type }}
</button>
}
@default {
<button mat-button (click)="openViewDialog(element)" color="navy-light">
<mat-icon fontIcon="description me-2"></mat-icon> {{ file.type }}
</button>
}
}
}
}
@if (!element.numero) {
<ngx-skeleton-loader [theme]="{width: '30px', height: '13px', marginBottom: '0px'}"></ngx-skeleton-loader>
}
</td>
</ng-container> </ng-container>
<!-- Actions Column --> <!-- Actions Column -->
<ng-container matColumnDef="actions"> <ng-container matColumnDef="actions">
@@ -247,13 +282,17 @@
<mat-icon>more_vert</mat-icon> <mat-icon>more_vert</mat-icon>
</button> </button>
<mat-menu [hidden]="!showAdd" #actionMenu="matMenu"> <mat-menu [hidden]="!showAdd" #actionMenu="matMenu">
<button mat-menu-item [routerLink]="['/jump', element.slug]">
<mat-icon aria-label="Voir" fontIcon="visibility" color="info" class="mini"></mat-icon>
<span class="text-info">Détails</span>
</button>
<button mat-menu-item (click)="openEditDialog(element)"> <button mat-menu-item (click)="openEditDialog(element)">
<mat-icon aria-label="Modifier" fontIcon="edit" color="primary" class="mini"></mat-icon> <mat-icon aria-label="Modifier" fontIcon="edit" color="primary" class="mini"></mat-icon>
<span class="text-primary">Modifier</span> <span class="text-primary">Modifier</span>
</button> </button>
<button mat-menu-item (click)="openDeleteDialog(element)"> <button mat-menu-item (click)="openDeleteDialog(element)">
<mat-icon aria-label="Supprimer" fontIcon="delete" color="danger" class="mini"></mat-icon> <mat-icon aria-label="Supprimer" fontIcon="delete" color="raspberry" class="mini"></mat-icon>
<span class="text-danger">Supprimer</span> <span class="text-raspberry">Supprimer</span>
</button> </button>
</mat-menu> </mat-menu>
} }
@@ -273,57 +312,3 @@
<tr mat-header-row *matHeaderRowDef="displayedColumns; sticky: true" class="accent"></tr> <tr mat-header-row *matHeaderRowDef="displayedColumns; sticky: true" class="accent"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;" class="text-middle accent"></tr> <tr mat-row *matRowDef="let row; columns: displayedColumns;" class="text-middle accent"></tr>
</table> </table>
<!--
<table mat-table [dataSource]="jumps">
<ng-container [matColumnDef]="col.key" *ngFor="let col of columnsSchema">
<th mat-header-cell *matHeaderCellDef [ngSwitch]="col.key">
<span *ngSwitchCase="'isSelected'">
<mat-checkbox (change)="selectAll($event)" [checked]="isAllSelected()" [indeterminate]="!isAllSelected() && isAnySelected()"></mat-checkbox>
</span>
<span *ngSwitchDefault>{{ col.label }}</span>
</th>
<td mat-cell *matCellDef="let element">
<div [ngSwitch]="col.type" *ngIf="!element.isEdit">
<ng-container *ngSwitchCase="'isSelected'">
<mat-checkbox (change)="element.isSelected = $event.checked" [checked]="element.isSelected">
</mat-checkbox>
</ng-container>
<div class="btn-edit" *ngSwitchCase="'isEdit'">
<button mat-button (click)="element.isEdit = !element.isEdit">
Modifier
</button>
<button mat-button class="button-remove" (click)="openDeleteDialog(element)">
Supprimer
</button>
</div>
<span *ngSwitchCase="'date'">
{{ element[col.key] | date: 'mediumDate' }}
</span>
<span *ngSwitchDefault>
{{ element[col.key] }}
</span>
</div>
<div [ngSwitch]="col.type" *ngIf="element.isEdit">
<div *ngSwitchCase="'isSelected'"></div>
<div class="btn-edit" *ngSwitchCase="'isEdit'">
<button mat-icon-button color="primary" [hidden]="!showAdd" (click)="openEditDialog(element)" [disabled]="disableSubmit(element.id)">
<mat-icon aria-label="Modifier" fontIcon="edit"></mat-icon>
</button>
</div>
<mat-form-field class="form-input" *ngSwitchCase="'date'" appearance="fill">
<mat-label>Choose a date</mat-label>
<input matInput [matDatepicker]="picker" [(ngModel)]="element[col.key]" />
<mat-datepicker-toggle matSuffix [for]="picker"></mat-datepicker-toggle>
<mat-datepicker #picker></mat-datepicker>
</mat-form-field>
<mat-form-field class="form-input" *ngSwitchDefault>
<input matInput [required]="col.required!" [pattern]="col.pattern!" [type]="col.type"
[(ngModel)]="element[col.key]" (change)="inputHandler($event, element.id, col.key)" />
</mat-form-field>
</div>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns"></tr>
</table>
-->
@@ -21,7 +21,7 @@ import { take } from 'rxjs/operators';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { FrenchPaginator } from 'src/app/components/shared/french-paginator.component'; import { FrenchPaginator } from 'src/app/components/shared/french-paginator.component';
import { ColumnDefinition, Errors, Jump, JumpList, JumpListConfig, jumpColumns } from 'src/app/core/models'; import { ColumnDefinition, Errors, Jump, JumpByDay, JumpFile, JumpFileType, JumpList, JumpListConfig, jumpColumns } from 'src/app/core/models';
import { JumpsService, JumpTableService } from 'src/app/core/services'; import { JumpsService, JumpTableService } from 'src/app/core/services';
import { JumpAddDialogComponent, JumpDeleteDialogComponent, JumpEditDialogComponent, JumpViewDialogComponent } from 'src/app/components/logbook/dialogs' import { JumpAddDialogComponent, JumpDeleteDialogComponent, JumpEditDialogComponent, JumpViewDialogComponent } from 'src/app/components/logbook/dialogs'
@@ -41,6 +41,7 @@ import { JumpAddDialogComponent, JumpDeleteDialogComponent, JumpEditDialogCompon
}) })
export class JumpTableComponent implements OnChanges, OnDestroy, AfterContentChecked { export class JumpTableComponent implements OnChanges, OnDestroy, AfterContentChecked {
private _jumps: Subscription = new Subscription(); private _jumps: Subscription = new Subscription();
private _subscriptions: Array<Subscription> = [];
//private _jump: Subscription = new Subscription(); //private _jump: Subscription = new Subscription();
private _query: JumpListConfig = { type: 'all', filters: {} }; private _query: JumpListConfig = { type: 'all', filters: {} };
//private _lastjump: Jump = {} as Jump; //private _lastjump: Jump = {} as Jump;
@@ -53,13 +54,13 @@ export class JumpTableComponent implements OnChanges, OnDestroy, AfterContentChe
'numero', 'date', 'lieu', 'oaci', 'aeronef', 'numero', 'date', 'lieu', 'oaci', 'aeronef',
'imat', 'hauteur', 'voile', 'taille', 'categorie', 'imat', 'hauteur', 'voile', 'taille', 'categorie',
'module', 'participants', 'programme', 'accessoires', 'module', 'participants', 'programme', 'accessoires',
'zone', 'video', 'actions' 'zone', 'files', 'actions'
];*/ ];*/
public displayedColumns: string[] = [ public displayedColumns: string[] = [
'numero', 'date', 'lieu', 'aeronef', 'numero', 'date', 'lieu', 'aeronef',
'hauteur', 'voile', 'categorie', 'hauteur', 'voile', 'categorie',
'participants', 'zone', 'accessoires', 'participants', 'zone', 'accessoires',
'video', 'actions' 'files', 'actions'
]; ];
//public displayedColumns: string[] = jumpColumns.map((col) => col.key) //public displayedColumns: string[] = jumpColumns.map((col) => col.key)
public columnsSchema: Array<ColumnDefinition> = jumpColumns; public columnsSchema: Array<ColumnDefinition> = jumpColumns;
@@ -115,7 +116,9 @@ export class JumpTableComponent implements OnChanges, OnDestroy, AfterContentChe
ngOnDestroy() { ngOnDestroy() {
this._jumps.unsubscribe(); this._jumps.unsubscribe();
//this._jump.unsubscribe(); this._subscriptions.forEach(subscription => {
subscription.unsubscribe();
});
} }
ngAfterContentChecked() { ngAfterContentChecked() {
@@ -155,6 +158,9 @@ export class JumpTableComponent implements OnChanges, OnDestroy, AfterContentChe
const jumps$: Observable<JumpList> = this._jumpsService.query(this._query); const jumps$: Observable<JumpList> = this._jumpsService.query(this._query);
this._jumps = jumps$.subscribe({ this._jumps = jumps$.subscribe({
next: (data: JumpList) => { next: (data: JumpList) => {
//this.updateJumps(data.jumps);
//this.linkSkydiverIdJumps();
//this.getKmlJumpFiles(data.jumps);
this.loading = false; this.loading = false;
this.jumps = new MatTableDataSource<Jump>(data.jumps); this.jumps = new MatTableDataSource<Jump>(data.jumps);
this.jumpsCount = data.jumpsCount; this.jumpsCount = data.jumpsCount;
@@ -170,12 +176,150 @@ export class JumpTableComponent implements OnChanges, OnDestroy, AfterContentChe
}); });
} }
linkSkydiverIdJumps() {
const config: JumpListConfig = { type: 'all', filters: {} } as JumpListConfig;
config.filters.limit = 200;
config.filters.offset = 0;
config.filters.dateRangeStart = '2024-01-01 00:00:00';
//const year = new Date().getFullYear();
//config.filters.dateRangeEnd = `${year}-12-31 23:59:59`;
config.filters.dateRangeEnd = '2024-12-31 23:59:59';
//console.log(config.filters);
const subscription: Subscription = this._jumpsService.getAllByDay(config)
.pipe(take(1))
.subscribe({
next: (days) => {
this._resetErrors();
//console.log('getAllByDay : ', days);
days.forEach(day => {
this.loadSkydiverIdJumps(day, config);
});
},
error: (err) => {
this.errors = err;
}
});
this._subscriptions.push(subscription);
}
loadSkydiverIdJumps(day: JumpByDay, config: JumpListConfig) {
config.filters.dateRangeStart = `${day.jumps[0].date!.substring(0, 10)} 00:00:00`;
config.filters.dateRangeEnd = `${day.jumps[0].date!.substring(0, 10)} 23:59:59`;
const subscription: Subscription = this._jumpsService.getAllFromSkydiverIdApi(config)
.pipe(take(1))
.subscribe({
next: (data) => {
if (data.items.length) {
const diff = day.count - data.items.length;
if (diff < 0) {
console.error(`${diff} ${diff > 1 ? 'données' : 'donnée'} altimètre de trop le ${day.jumps[0].date!.substring(0, 10)}`);
} else {
if (diff > 0) {
console.error(`${diff}/${day.count} ${diff > 1 ? 'données altimètre manquantes' : 'donnée altimètre manquante'} le ${day.jumps[0].date!.substring(0, 10)}`, day.jumps, data.items);
}
console.log(`${day.count} ${day.count > 1 ? 'sauts' : 'saut'} le ${data.items[0].date!.substring(0, 10)}`, day.jumps.sort((a, b) => b.numero! - a.numero!), data.items);
data.items.map((jump, index) => {
const file: JumpFile = {
name: jump.name,
path: `/Volumes/Storage/Skydive/X2_Logs/${day.jumps[index].date!.substring(0, 4)}/`,
type: JumpFileType.CSV
} as JumpFile;
this._subscriptions.push(
this._jumpsService.saveX2Data(day.jumps[index].slug!, file, jump)
.pipe(take(1))
.subscribe({
next: (jump) => {
this._resetErrors();
console.log(`Le fichier ${file.name} a été ajouté au saut n°${jump.numero} !`, 'OK');
},
error: (err) => {
this.errors = err;
}
})
);
return jump;
});
}
//console.log(`${day.count} ${day.count > 1 ? 'sauts' : 'saut'} le ${day.jumps[0].date!.substring(0, 10)}`);
//console.log(`${data.items.length} ${data.items.length > 1 ? 'sauts' : 'saut'} le ${data.items[0].date!.substring(0, 10)}`, data);
} else {
console.info('Aucune données altimètre pour le ', day.jumps[0].date!.substring(0, 10));
}
},
error: (err) => {
console.error('getAllFromSkydiverIdApi error');
this.errors = err;
}
});
this._subscriptions.push(subscription);
}
updateJumps(jumps: Array<Jump>) {
jumps.map(jump => {
jump.files = [];
if (jump.dossier !== "" && jump.video !== "") {
const file: JumpFile = {
name: jump.video!,
path: jump.dossier!,
type: JumpFileType.VIDEO
} as JumpFile;
this._subscriptions.push(
this._jumpsService.saveFile(jump.slug, file)
.pipe(take(1))
.subscribe({
next: (file) => {
this._resetErrors();
jump.files.push(file);
console.log(`Le fichier ${file.name} a été ajouté au saut n°${jump.numero} !`, 'OK');
},
error: (err) => {
this.errors = err;
}
})
);
}
return jump;
});
}
getKmlJumpFiles(jumps: Array<Jump>) {
jumps.map(jump => {
if (jump.x2data !== null) {
//console.log(`Le fichier ${jump.x2data.name} existe pour le saut n°${jump.numero} !`, `https://skydiver.id/api/jump/${jump.x2data.id}/kml`);
const file: JumpFile = {
name: jump.x2data.name.substring(0, -4) + '.kml',
path: `/X2_Kmls/${jump.date.substring(0, 4)}/`,
type: JumpFileType.KML
} as JumpFile;
this._subscriptions.push(
this._jumpsService.saveKml(jump.slug, file)
.pipe(take(1))
.subscribe({
next: (file) => {
this._resetErrors();
console.log(`Le fichier ${file.path}${file.name} a été ajouté au saut n°${jump.numero} !`, 'OK');
},
error: (err) => {
this.errors = err;
}
})
);
/*
*/
}
return jump;
});
}
deleteJump(slug: string) { deleteJump(slug: string) {
this.isDeleting = true; this.isDeleting = true;
this._jumpsService.destroy(slug).pipe(take(1)) this._subscriptions.push(
this._jumpsService.destroy(slug)
.pipe(take(1))
.subscribe(() => { .subscribe(() => {
this.router.navigateByUrl('/'); this.router.navigateByUrl('/');
}); })
);
} }
openDeleteDialog(jump: Jump): void { openDeleteDialog(jump: Jump): void {
@@ -183,12 +327,15 @@ export class JumpTableComponent implements OnChanges, OnDestroy, AfterContentChe
width: '60vw', width: '60vw',
data: jump data: jump
}); });
dialogRef.afterClosed().pipe(take(1)) this._subscriptions.push(
dialogRef.afterClosed()
.pipe(take(1))
.subscribe(result => { .subscribe(result => {
if (result != undefined) { if (result != undefined) {
this._onDelete(result.slug); this._onDelete(result.slug);
} }
}); })
);
} }
openEditDialog(jump: Jump): void { openEditDialog(jump: Jump): void {
@@ -196,12 +343,15 @@ export class JumpTableComponent implements OnChanges, OnDestroy, AfterContentChe
width: '70vw', width: '70vw',
data: jump data: jump
}); });
dialogRef.afterClosed().pipe(take(1)) this._subscriptions.push(
.subscribe(result => { dialogRef.afterClosed()
if (result != undefined) { .pipe(take(1))
this._onEdit(result); .subscribe(result => {
} if (result != undefined) {
}); this._onEdit(result);
}
})
);
} }
openViewDialog(jump: Jump): void { openViewDialog(jump: Jump): void {
@@ -230,20 +380,22 @@ export class JumpTableComponent implements OnChanges, OnDestroy, AfterContentChe
private _onDelete(slug: string): void { private _onDelete(slug: string): void {
try { try {
this.isDeleting = true; this.isDeleting = true;
this._jumpsService.destroy(slug) this._subscriptions.push(
.pipe(take(1)) this._jumpsService.destroy(slug)
.subscribe({ .pipe(take(1))
next: () => { .subscribe({
this._resetErrors(); next: () => {
this._openSnackBar(`Le saut a été supprimé !`, 'OK'); this._resetErrors();
this.runQuery(); this._openSnackBar(`Le saut a été supprimé !`, 'OK');
this.tableRefresh = !this.tableRefresh; this.runQuery();
this._jumpTableService.updateTableRefresh(this.tableRefresh); this.tableRefresh = !this.tableRefresh;
}, this._jumpTableService.updateTableRefresh(this.tableRefresh);
error: (err) => { },
this.errors = err; error: (err) => {
} this.errors = err;
}); }
})
);
} catch (error) { } catch (error) {
console.error(error); console.error(error);
} }
@@ -252,21 +404,23 @@ export class JumpTableComponent implements OnChanges, OnDestroy, AfterContentChe
private _onEdit(jump: Jump): void { private _onEdit(jump: Jump): void {
try { try {
this.isSubmitting = true; this.isSubmitting = true;
this._jumpsService.update(jump) this._subscriptions.push(
.pipe(take(1)) this._jumpsService.update(jump)
.subscribe({ .pipe(take(1))
next: (jump) => { .subscribe({
this._resetErrors(); next: (jump) => {
this._openSnackBar(`Le saut n°${jump.numero} a été mis à jour !`, 'OK'); this._resetErrors();
this.runQuery(); this._openSnackBar(`Le saut n°${jump.numero} a été mis à jour !`, 'OK');
this.tableRefresh = !this.tableRefresh; this.runQuery();
this._jumpTableService.updateTableRefresh(this.tableRefresh); this.tableRefresh = !this.tableRefresh;
}, this._jumpTableService.updateTableRefresh(this.tableRefresh);
error: (err) => { },
this.errors = err; error: (err) => {
this.isSubmitting = false; this.errors = err;
} this.isSubmitting = false;
}); }
})
);
} catch (error) { } catch (error) {
console.error(error); console.error(error);
} }
@@ -1,6 +1,6 @@
<mat-toolbar class="topbar bg-primary"> <mat-toolbar class="topbar bg-primary">
<div class="navbar-header"> <div class="navbar-header">
<a class="navbar-brand" href="index.html"> <a class="navbar-brand" href="/">
<span class="logo"> <span class="logo">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="190px" height="33px" viewBox="0 0 190 33" enable-background="new 0 0 190 33" xml:space="preserve"> <svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="190px" height="33px" viewBox="0 0 190 33" enable-background="new 0 0 190 33" xml:space="preserve">
<path id="Jumper_2" fill="#FFFFFF" <path id="Jumper_2" fill="#FFFFFF"
@@ -281,8 +281,8 @@
<app-layout-footer></app-layout-footer> <app-layout-footer></app-layout-footer>
</mat-sidenav> </mat-sidenav>
<mat-sidenav-content class="page-wrapper"> <mat-sidenav-content class="page-wrapper">
<div class="page-content"> <!--<div class="page-content">-->
<router-outlet><!--<app-spinner></app-spinner>--></router-outlet> <router-outlet><!--<app-spinner></app-spinner>--></router-outlet>
</div> <!--</div>-->
</mat-sidenav-content> </mat-sidenav-content>
</mat-sidenav-container> </mat-sidenav-container>
@@ -1,5 +1,4 @@
import { Component, OnInit, OnDestroy, ChangeDetectorRef } from '@angular/core'; import { Component, OnInit, OnDestroy, ChangeDetectorRef } from '@angular/core';
import { Router, RouterOutlet, RouterModule } from '@angular/router'; import { Router, RouterOutlet, RouterModule } from '@angular/router';
import { MediaMatcher } from '@angular/cdk/layout'; import { MediaMatcher } from '@angular/cdk/layout';
import { MatBadgeModule } from '@angular/material/badge'; import { MatBadgeModule } from '@angular/material/badge';
File diff suppressed because one or more lines are too long
+12 -4
View File
@@ -11,14 +11,19 @@ const MENUAUTH: Menu[] = [
const MENUITEMS: Menu[] = [ const MENUITEMS: Menu[] = [
{ state: 'dashboard', type: 'link', name: 'Dashboard', icon: 'dashboard', desc: '' }, { state: 'dashboard', type: 'link', name: 'Dashboard', icon: 'dashboard', desc: '' },
{ state: 'logbook', type: 'link', name: 'Carnet de saut', icon: 'assignment_turned_in', desc: '' }, { state: 'logbook', type: 'link', name: 'Carnet de saut', icon: 'assignment_turned_in', desc: '' },
{ state: 'aeronef', type: 'link', name: 'Aéronefs', icon: 'flight', desc: '' }, { state: 'aeronefs', type: 'link', name: 'Aéronefs', icon: 'flight', desc: '' },
{ state: 'canopy', type: 'link', name: 'Voiles', icon: 'paragliding', desc: '' }, { state: 'dropzones', type: 'link', name: 'Drop Zones', icon: 'flight_takeoff', desc: '' },
{ state: 'dropzone', type: 'link', name: 'Dropzones', icon: 'adjust', desc: '' }, { state: 'canopies', type: 'link', name: 'Voiles', icon: 'paragliding', desc: '' },
{ state: 'jump', type: 'link', name: 'Sauts', icon: 'cloud_download', desc: '' }, { state: 'jumps', type: 'link', name: 'Sauts', icon: 'filter_drama', desc: '' },
{ state: 'calculator', type: 'link', name: 'Taille de voile', icon: 'calculate', desc: '' }, { state: 'calculator', type: 'link', name: 'Taille de voile', icon: 'calculate', desc: '' },
{ state: 'qcm', type: 'menu', name: 'QCM Brevets', icon: 'rule_folder', desc: '' } { state: 'qcm', type: 'menu', name: 'QCM Brevets', icon: 'rule_folder', desc: '' }
]; ];
const MENUPJUMP: Menu[] = [
{ state: '#', type: 'link', name: 'Modifier', icon: 'edit', desc: '', color: 'primary' },
{ state: '#', type: 'link', name: 'Supprimer', icon: 'delete', desc: '', color: 'raspberry' }
];
const MENUQCM: Menu[] = [ const MENUQCM: Menu[] = [
{ state: 'bpa', type: 'link', name: 'BPA', icon: 'rule' }, { state: 'bpa', type: 'link', name: 'BPA', icon: 'rule' },
{ state: 'c', type: 'link', name: 'Brevet C', icon: 'rule' }, { state: 'c', type: 'link', name: 'Brevet C', icon: 'rule' },
@@ -63,4 +68,7 @@ export class MenuItems {
getMenuPanel(): Menu[] { getMenuPanel(): Menu[] {
return MENUPANEL; return MENUPANEL;
} }
getMenuJump(): Menu[] {
return MENUPJUMP;
}
} }
@@ -11,7 +11,8 @@ export class ShowAuthedDirective implements OnInit, OnDestroy {
private _auth: Subscription = new Subscription(); private _auth: Subscription = new Subscription();
constructor( constructor(
private templateRef: TemplateRef<any>, //private templateRef: TemplateRef<any>,
private templateRef: TemplateRef<never>,
private userService: UserService, private userService: UserService,
private viewContainer: ViewContainerRef private viewContainer: ViewContainerRef
) { } ) { }
+2 -2
View File
@@ -1,10 +1,10 @@
import { inject } from '@angular/core'; import { inject } from '@angular/core';
import { ActivatedRouteSnapshot, CanActivateFn, Router, RouterStateSnapshot } from '@angular/router'; import { CanActivateFn, Router } from '@angular/router';
import { map, take } from 'rxjs/operators'; import { map, take } from 'rxjs/operators';
import { UserService } from 'src/app/core/services'; import { UserService } from 'src/app/core/services';
export const adminGuard: CanActivateFn = (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) => { export const adminGuard: CanActivateFn = () => {
const router = inject(Router); const router = inject(Router);
const userService = inject(UserService); const userService = inject(UserService);
return userService.currentUser.pipe(take(1)).pipe(map(data => { return userService.currentUser.pipe(take(1)).pipe(map(data => {
+2 -2
View File
@@ -1,10 +1,10 @@
import { inject } from '@angular/core'; import { inject } from '@angular/core';
import { ActivatedRouteSnapshot, CanActivateFn, Router, RouterStateSnapshot } from '@angular/router'; import { CanActivateFn, Router } from '@angular/router';
import { map, take } from 'rxjs/operators'; import { map, take } from 'rxjs/operators';
import { UserService } from 'src/app/core/services'; import { UserService } from 'src/app/core/services';
export const authGuard: CanActivateFn = (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) => { export const authGuard: CanActivateFn = () => {
const router = inject(Router); const router = inject(Router);
const userService = inject(UserService); const userService = inject(UserService);
return userService.isAuthenticated.pipe(take(1)).pipe(map(data => { return userService.isAuthenticated.pipe(take(1)).pipe(map(data => {
+2 -2
View File
@@ -1,11 +1,11 @@
import { inject } from '@angular/core'; import { inject } from '@angular/core';
import { ActivatedRouteSnapshot, CanActivateFn, Router, RouterStateSnapshot } from '@angular/router'; import { CanActivateFn } from '@angular/router';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { map, take } from 'rxjs/operators'; import { map, take } from 'rxjs/operators';
import { UserService } from 'src/app/core/services'; import { UserService } from 'src/app/core/services';
export const noauthGuard: CanActivateFn = (route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> => { export const noauthGuard: CanActivateFn = (): Observable<boolean> => {
/* /*
const router = inject(Router); const router = inject(Router);
const userService = inject(UserService); const userService = inject(UserService);
@@ -0,0 +1,6 @@
import { Application } from './application.model';
export interface ApplicationList {
applications: Array<Application>;
applicationsCount: number;
}
-13
View File
@@ -1,13 +0,0 @@
export interface CanopySize {
id: string;
key: any;
value: any;
doc?: CanopySizeDoc;
}
export interface CanopySizeDoc {
_id: string;
_rev: string;
type: string;
weight: number;
values: [];
}
+5 -5
View File
@@ -3,27 +3,27 @@ import { ChartConfiguration, ChartOptions } from 'chart.js';
export interface BarConfig { export interface BarConfig {
barChartData: ChartConfiguration<'bar'>['data'], barChartData: ChartConfiguration<'bar'>['data'],
barChartOptions: ChartConfiguration<'bar'>['options'], barChartOptions: ChartConfiguration<'bar'>['options'],
barChartPlugins: any[], barChartPlugins: ChartConfiguration<'bar'>['plugins'],
barChartLegend: boolean barChartLegend: boolean
} }
export interface CircleConfig { export interface CircleConfig {
circleChartLabels: string[], circleChartLabels: string[],
circleChartDatasets: ChartConfiguration<'doughnut'>['data']['datasets'], circleChartDatasets: ChartConfiguration<'doughnut'>['data']['datasets'],
circleChartOptions: ChartConfiguration<'doughnut'>['options'], circleChartOptions: ChartConfiguration<'doughnut'>['options'],
circleChartPlugins: ChartConfiguration<'doughnut'>['plugins'], circleChartPlugins: ChartConfiguration<'doughnut'>['plugins'],
circleChartLegend: boolean circleChartLegend: boolean
} }
export interface DoughnutConfig { export interface DoughnutConfig {
doughnutChartLabels: string[], doughnutChartLabels: string[],
doughnutChartDatasets: ChartConfiguration<'doughnut'>['data']['datasets'], doughnutChartDatasets: ChartConfiguration<'doughnut'>['data']['datasets'],
doughnutChartOptions: ChartConfiguration<'doughnut'>['options'], doughnutChartOptions: ChartConfiguration<'doughnut'>['options'],
doughnutChartLegend: boolean doughnutChartLegend: boolean
} }
export interface LineConfig { export interface LineConfig {
lineChartData: ChartConfiguration<'line'>['data'], lineChartData: ChartConfiguration<'line'>['data'],
lineChartOptions: ChartOptions<'line'>, lineChartOptions: ChartOptions<'line'>,
lineChartLegend: boolean lineChartLegend: boolean
} }

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