Import des sources angular à partir de 'headup_app'

This commit is contained in:
Rampeur
2025-08-11 23:26:29 +02:00
parent 7dcb426ef5
commit 0a6cbc0c00
335 changed files with 64362 additions and 0 deletions
+55
View File
@@ -0,0 +1,55 @@
import { Directive, AfterContentChecked } from '@angular/core';
import { Router, NavigationEnd } from '@angular/router';
import { AccordionLinkDirective } from './accordionlink.directive';
import { filter } from 'rxjs/operators';
@Directive({
selector: '[appAccordion]',
standalone: true
})
export class AccordionDirective implements AfterContentChecked {
protected navlinks: Array<AccordionLinkDirective> = [];
closeOtherLinks(selectedLink: AccordionLinkDirective): void {
this.navlinks.forEach((link: AccordionLinkDirective) => {
if (link !== selectedLink) {
link.selected = false;
}
});
}
addLink(link: AccordionLinkDirective): void {
this.navlinks.push(link);
}
removeGroup(link: AccordionLinkDirective): void {
const index = this.navlinks.indexOf(link);
if (index !== -1) {
this.navlinks.splice(index, 1);
}
}
checkOpenLinks() {
this.navlinks.forEach((link: AccordionLinkDirective) => {
if (link.group) {
const routeUrl = this.router.url;
const currentUrl = routeUrl.split('/');
if (currentUrl.indexOf(link.group) > 0) {
link.selected = true;
this.closeOtherLinks(link);
}
}
});
}
ngAfterContentChecked(): void {
this.router.events
.pipe(filter(event => event instanceof NavigationEnd))
.subscribe(() => this.checkOpenLinks());
}
constructor(private router: Router) {
setTimeout(() => this.checkOpenLinks());
}
}
@@ -0,0 +1,20 @@
import { Directive, HostListener, Inject } from '@angular/core';
import { AccordionLinkDirective } from './accordionlink.directive';
@Directive({
selector: '[appAccordionToggle]',
standalone: true
})
export class AccordionAnchorDirective {
protected navlink: AccordionLinkDirective;
constructor(@Inject(AccordionLinkDirective) navlink: AccordionLinkDirective) {
this.navlink = navlink;
}
@HostListener('click', ['$event'])
onClick() {
this.navlink.toggle();
}
}
@@ -0,0 +1,51 @@
import {
Directive,
HostBinding,
Inject,
Input,
OnInit,
OnDestroy
} from '@angular/core';
import { AccordionDirective } from './accordion.directive';
@Directive({
selector: '[appAccordionLink]',
standalone: true
})
export class AccordionLinkDirective implements OnInit, OnDestroy {
@Input()
public group!: string;
@HostBinding('class.selected')
@Input()
get selected(): boolean {
return this._selected;
}
set selected(value: boolean) {
this._selected = value;
if (value) {
this.nav.closeOtherLinks(this);
}
}
protected _selected: boolean = false;
protected nav: AccordionDirective;
constructor(@Inject(AccordionDirective) nav: AccordionDirective) {
this.nav = nav;
}
ngOnInit() {
this.nav.addLink(this);
}
ngOnDestroy(){
this.nav.removeGroup(this);
}
toggle() {
this.selected = !this.selected;
}
}
+3
View File
@@ -0,0 +1,3 @@
export * from './accordionanchor.directive';
export * from './accordionlink.directive';
export * from './accordion.directive';
@@ -0,0 +1,17 @@
import { FormGroup } from '@angular/forms';
export function ConfirmedValidator(controlName: string, matchingControlName: string){
return (formGroup: FormGroup) => {
const control = formGroup.controls[controlName];
const matchingControl = formGroup.controls[matchingControlName];
if (matchingControl.errors && !matchingControl.errors['confirmedValidator']) {
return;
}
if (control.value !== matchingControl.value) {
matchingControl.setErrors({ confirmedValidator: true });
} else {
matchingControl.setErrors(null);
}
}
}
@@ -0,0 +1,42 @@
<mat-card>
<mat-card-header>
<mat-card-title>{{ title }}</mat-card-title>
<mat-card-subtitle>{{ subtitle }}</mat-card-subtitle>
<span class="flex-spacer"></span>
<button mat-icon-button [matMenuTriggerFor]="menuAeronefBar" aria-label="Menu Aeronef Bar">
<mat-icon fontIcon="more_vert"></mat-icon>
</button>
<mat-menu #menuAeronefBar="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>
</mat-card-header>
<mat-divider class="my-3"></mat-divider>
<mat-card-content>
@if (displayCharts) {
<app-bar-horizontal-chart [headers]="seriesHeader" [names]="seriesName" [values]="seriesValue" [colors]="seriesColor" label="Nombre total de sauts" [legend]="false"></app-bar-horizontal-chart>
}
</mat-card-content>
<mat-divider class="mt-3 mb-0"></mat-divider>
<mat-accordion>
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title>Historique annuel</mat-panel-title>
</mat-expansion-panel-header>
<app-history-table [headers]="seriesHeader" [names]="seriesName" [values]="seriesValue" [rows]="seriesRow" [colors]="seriesColorClass"></app-history-table>
<!--
<div fxLayout="row wrap">
<div *ngFor='let name of seriesName; let i=index' fxFlex.gt-lg="33" fxFlex.gt-md="50" fxFlex.gt-xs="20" fxFlex="100">
<h6 class="{{seriesColor[i]}} m-0">
<i class="mdi mdi-checkbox-blank-circle font-10 m-r-10 "></i>{{name}} - {{seriesValue[i]}}
</h6>
</div>
</div>
-->
</mat-expansion-panel>
</mat-accordion>
</mat-card>
@@ -0,0 +1,25 @@
import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing';
import { AeronefsBarComponent } from './aeronefs-bar.component';
describe('AeronefsBarComponent', () => {
let component: AeronefsBarComponent;
let fixture: ComponentFixture<AeronefsBarComponent>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
imports: [AeronefsBarComponent]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(AeronefsBarComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,95 @@
import { Component, DestroyRef, inject, OnInit, OnDestroy } from '@angular/core';
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { 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 { BarHorizontalChartComponent } from 'src/app/components/shared/helpers-chart';
import { HistoryTableComponent } from 'src/app/components/shared/helpers-jump';
import { UtilitiesService, AeronefsService } from 'src/app/core/services';
import { AeronefByImat, AeronefByYear } from 'src/app/core/models';
@Component({
selector: 'app-aeronefs-bar',
standalone: true,
imports: [
MatButtonModule, MatCardModule, MatDividerModule,
MatExpansionModule, MatIconModule, MatMenuModule,
BarHorizontalChartComponent, HistoryTableComponent
],
templateUrl: './aeronefs-bar.component.html'
})
export class AeronefsBarComponent implements OnInit, OnDestroy {
private _aeronefByImat: Subscription = new Subscription();
private _aeronefByYear: Subscription = new Subscription();
private _aeronefsByImat!: Array<AeronefByImat>;
private _aeronefsByYear!: Array<AeronefByYear>;
public title: string = 'Aéronefs';
public subtitle: string = 'Nombre total de sauts par aéronef';
public seriesHeader: string[] = [];
public seriesName: string[] = [];
public seriesValue: number[] = [];
public seriesRow: Array<Array<number>> = [];
public seriesColor: {
backgroundColor: string[],
borderColor: string[]
} = {
backgroundColor: this._utilitiesService.getSeriesColors(0.8, 'all'),
borderColor: this._utilitiesService.getSeriesColors(1, 'all')
};
public seriesColorClass: string[] = this._utilitiesService.getChartColors();
public displayCharts = false;
public destroyRef = inject(DestroyRef);
constructor(
private _aeronefsService: AeronefsService,
private _utilitiesService: UtilitiesService,
public menuItems: MenuItems
) { }
ngOnInit() {
this._loadAeronefByImat();
}
ngOnDestroy() {
this._aeronefByImat.unsubscribe();
this._aeronefByYear.unsubscribe();
}
private _loadAeronefByImat(): void {
const aeronefs$: Observable<Array<AeronefByImat>> = this._aeronefsService.getAllByImat().pipe(takeUntilDestroyed(this.destroyRef));
this._aeronefByImat = aeronefs$.subscribe((aggregate: Array<AeronefByImat>) => {
this._aeronefsByImat = aggregate.map((row: AeronefByImat) => {
//this.chartConfig.barChartData.labels!.push(`${row.aeronef} ${row.imat}`);
this.seriesName.push(`${row.aeronef} ${row.imat}`);
this.seriesValue.push(row.count);
return row;
});
this._loadAeronefByYear();
});
}
private _loadAeronefByYear(): void {
this.seriesHeader = ['2018', '2019', '2020', '2021', '2022', '2023', '2024'];
const values: Array<number> = this.seriesHeader.map(() => 0);
this.seriesName.forEach(() => {
this.seriesRow.push([...values]);
});
const dropzones$: Observable<Array<AeronefByYear>> = this._aeronefsService.getAllByImatByYear().pipe(takeUntilDestroyed(this.destroyRef));
this._aeronefByYear = dropzones$.subscribe((aggregate: Array<AeronefByYear>) => {
this._aeronefsByYear = aggregate.map((row: AeronefByYear) => {
const aeronef: string = row.aeronef;
const imat: string = row.imat;
const year: string = row.year.toString();
this.seriesRow[this.seriesName.indexOf(`${aeronef} ${imat}`)][this.seriesHeader.indexOf(year)] = row.count;
return row;
});
this.displayCharts = true;
});
}
}
@@ -0,0 +1,42 @@
<mat-card>
<mat-card-header>
<mat-card-title>{{ title }}</mat-card-title>
<mat-card-subtitle>{{ subtitle }}</mat-card-subtitle>
<span class="flex-spacer"></span>
<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>
</mat-card-header>
<mat-divider class="my-3"></mat-divider>
<mat-card-content>
<app-pie-chart [headers]="seriesHeader" [names]="seriesName" [values]="seriesValue" [colors]="seriesColor"></app-pie-chart>
</mat-card-content>
<mat-divider class="mt-3 mb-0"></mat-divider>
<mat-accordion>
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title>Historique annuel</mat-panel-title>
</mat-expansion-panel-header>
<app-history-table [headers]="seriesHeader" [names]="seriesName" [values]="seriesValue" [rows]="seriesRow" [colors]="seriesColorClass"></app-history-table>
</mat-expansion-panel>
</mat-accordion>
<!--
<mat-divider class="my-3"></mat-divider>
<mat-card-content>
<div fxLayout="row wrap">
<div *ngFor='let name of seriesName; let i=index' fxFlex.gt-lg="33" fxFlex.gt-md="50" fxFlex.gt-xs="20" fxFlex="100">
<h6 class="{{seriesColor[i]}} m-0">
<i class="mdi mdi-checkbox-blank-circle font-10 m-r-10 "></i>{{name}} - {{seriesValue[i]}}
</h6>
</div>
</div>
</mat-card-content>-->
</mat-card>
@@ -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();
});
});
@@ -0,0 +1,93 @@
import { Component, DestroyRef, inject, OnInit, OnDestroy } from '@angular/core';
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { MatBadgeModule } from '@angular/material/badge';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatDividerModule } from '@angular/material/divider';
import { MatExpansionModule } from '@angular/material/expansion';
import { MatIconModule } from '@angular/material/icon';
import { MatMenuModule } from '@angular/material/menu';
import { Observable, Subscription } from 'rxjs';
import { MenuItems } from 'src/app/components/shared';
import { HistoryTableComponent } from 'src/app/components/shared/helpers-jump';
import { PieChartComponent } from 'src/app/components/shared/helpers-chart';
import { UtilitiesService, AeronefsService } from 'src/app/core/services';
import { AeronefByImat, AeronefByYear } from 'src/app/core/models';
@Component({
selector: 'app-aeronefs-pie',
standalone: true,
imports: [
MatBadgeModule, MatButtonModule, MatCardModule, MatDividerModule,
MatExpansionModule, MatIconModule, MatMenuModule,
HistoryTableComponent, PieChartComponent
],
templateUrl: './aeronefs-pie.component.html'
})
export class AeronefsPieComponent implements OnInit, OnDestroy {
private _aeronefByImat: Subscription = new Subscription();
private _aeronefByYear: Subscription = new Subscription();
private _aeronefsByImat!: Array<AeronefByImat>;
private _aeronefsByYear!: Array<AeronefByYear>;
public title: string = 'Aéronefs';
public subtitle: string = 'Nombre total de sauts par aéronef';
public seriesHeader: string[] = [];
public seriesName: string[] = [];
public seriesValue: number[] = [];
public seriesRow: Array<Array<number>> = [];
public seriesColor: {
backgroundColor: string[],
borderColor: string[]
} = {
backgroundColor: this._utilitiesService.getSeriesColors(0.8, 'all'),
borderColor: this._utilitiesService.getSeriesColors(1, 'all')
};
public seriesColorClass: string[] = this._utilitiesService.getChartColors();
public destroyRef = inject(DestroyRef);
constructor(
private _aeronefsService: AeronefsService,
private _utilitiesService: UtilitiesService,
public menuItems: MenuItems
) { }
ngOnInit() {
this._loadAeronefByImat();
}
ngOnDestroy() {
this._aeronefByImat.unsubscribe();
this._aeronefByYear.unsubscribe();
}
private _loadAeronefByImat(): void {
const aeronefs$: Observable<Array<AeronefByImat>> = this._aeronefsService.getAllByImat().pipe(takeUntilDestroyed(this.destroyRef));
this._aeronefByImat = aeronefs$.subscribe((aggregate: Array<AeronefByImat>) => {
this._aeronefsByImat = aggregate.map((row: AeronefByImat) => {
this.seriesName.push(`${row.aeronef} ${row.imat}`);
this.seriesValue.push(row.count);
return row;
});
this._loadAeronefByYear();
});
}
private _loadAeronefByYear(): void {
this.seriesHeader = ['2018', '2019', '2020', '2021', '2022', '2023', '2024'];
const values: Array<number> = this.seriesHeader.map(() => 0);
this.seriesName.forEach(() => {
this.seriesRow.push([...values]);
});
const dropzones$: Observable<Array<AeronefByYear>> = this._aeronefsService.getAllByImatByYear().pipe(takeUntilDestroyed(this.destroyRef));
this._aeronefByYear = dropzones$.subscribe((aggregate: Array<AeronefByYear>) => {
this._aeronefsByYear = aggregate.map((row: AeronefByYear) => {
const aeronef: string = row.aeronef;
const imat: string = row.imat;
const year: string = row.year.toString();
this.seriesRow[this.seriesName.indexOf(`${aeronef} ${imat}`)][this.seriesHeader.indexOf(year)] = row.count;
return row;
});
});
}
}
@@ -0,0 +1,31 @@
<mat-card>
<mat-card-header>
<mat-card-title>{{ title }}</mat-card-title>
<mat-card-subtitle>{{ subtitle }}</mat-card-subtitle>
<span class="flex-spacer"></span>
<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>
</mat-card-header>
<mat-divider class="my-3"></mat-divider>
<mat-card-content>
<app-pie-chart [headers]="seriesHeader" [names]="seriesName" [values]="seriesValue" [colors]="seriesColor"></app-pie-chart>
</mat-card-content>
<mat-divider class="mt-3 mb-0"></mat-divider>
<mat-accordion>
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title>Historique annuel</mat-panel-title>
</mat-expansion-panel-header>
<app-history-table [headers]="seriesHeader" [names]="seriesName" [values]="seriesValue" [rows]="seriesRow" [colors]="seriesColorClass"></app-history-table>
</mat-expansion-panel>
</mat-accordion>
</mat-card>
@@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { CanopiesModelsComponent } from './canopies-models.component';
describe('CanopiesModelsComponent', () => {
let component: CanopiesModelsComponent;
let fixture: ComponentFixture<CanopiesModelsComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [CanopiesModelsComponent]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(CanopiesModelsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,92 @@
import { Component, DestroyRef, inject, OnInit, OnDestroy} from '@angular/core';
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
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 { PieChartComponent } from 'src/app/components/shared/helpers-chart';
import { HistoryTableComponent } from 'src/app/components/shared/helpers-jump';
import { UtilitiesService, CanopiesService } from 'src/app/core/services';
import { CanopyModelBySize, CanopyModelByYear } from 'src/app/core/models';
@Component({
selector: 'app-canopies-models',
standalone: true,
imports: [
MatButtonModule, MatCardModule, MatDividerModule,
MatExpansionModule, MatIconModule, MatMenuModule,
PieChartComponent, HistoryTableComponent
],
templateUrl: './canopies-models.component.html'
})
export class CanopiesModelsComponent implements OnInit, OnDestroy {
private _canopyBySize: Subscription = new Subscription();
private _canopyByYear: Subscription = new Subscription();
private _canopiesBySize!: Array<CanopyModelBySize>;
private _canopiesByYear!: Array<CanopyModelByYear>;
public title: string = 'Modèles de voile';
public subtitle: string = 'Nombre total de sauts par modèle';
public seriesHeader: string[] = [];
public seriesName: string[] = [];
public seriesValue: number[] = [];
public seriesRow: Array<Array<number>> = [];
public seriesColor: {
backgroundColor: string[],
borderColor: string[]
} = {
backgroundColor: this._utilitiesService.getSeriesColors(0.8, 'all'),
borderColor: this._utilitiesService.getSeriesColors(1, 'all')
};
public seriesColorClass: string[] = this._utilitiesService.getChartColors();
public destroyRef = inject(DestroyRef);
constructor(
private _canopiesService: CanopiesService,
private _utilitiesService: UtilitiesService,
public menuItems: MenuItems
) { }
ngOnInit() {
this._loadCanopyModelBySize();
}
ngOnDestroy() {
this._canopyBySize.unsubscribe();
this._canopyByYear.unsubscribe();
}
private _loadCanopyModelBySize(): void {
const canopies$: Observable<Array<CanopyModelBySize>> = this._canopiesService.getAllBySizeByModel().pipe(takeUntilDestroyed(this.destroyRef));
this._canopyBySize = canopies$.subscribe((aggregate: Array<CanopyModelBySize>) => {
this._canopiesBySize = aggregate.map((row: CanopyModelBySize) => {
this.seriesName.push(`${row.taille.toString()} - ${row.voile}`);
this.seriesValue.push(row.count);
return row;
});
this._loadCanopyModelByYear();
});
}
private _loadCanopyModelByYear(): void {
this.seriesHeader = ['2018', '2019', '2020', '2021', '2022', '2023', '2024'];
const values: Array<number> = this.seriesHeader.map(() => 0);
this.seriesName.forEach(() => {
this.seriesRow.push([...values]);
});
const dropzones$: Observable<Array<CanopyModelByYear>> = this._canopiesService.getAllBySizeByModelByYear().pipe(takeUntilDestroyed(this.destroyRef));
this._canopyByYear = dropzones$.subscribe((aggregate: Array<CanopyModelByYear>) => {
this._canopiesByYear = aggregate.map((row: CanopyModelByYear) => {
const voile: string = row.voile;
const taille: string = row.taille.toString();
const year: string = row.year.toString();
this.seriesRow[this.seriesName.indexOf(`${taille} - ${voile}`)][this.seriesHeader.indexOf(year)] = row.count;
return row;
});
});
}
}
@@ -0,0 +1,42 @@
<mat-card>
<mat-card-header>
<mat-card-title>{{ title }}</mat-card-title>
<mat-card-subtitle>{{ subtitle }}</mat-card-subtitle>
<span class="flex-spacer"></span>
<button mat-icon-button [matMenuTriggerFor]="menuCanopySize" aria-label="Menu Canopy Size">
<mat-icon fontIcon="more_vert"></mat-icon>
</button>
<mat-menu #menuCanopySize="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>
</mat-card-header>
<mat-divider class="my-3"></mat-divider>
<mat-card-content>
<app-pie-chart [headers]="seriesHeader" [names]="seriesName" [values]="seriesValue" [colors]="seriesColor">
</app-pie-chart>
</mat-card-content>
<mat-divider class="mt-3 mb-0"></mat-divider>
<mat-accordion>
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title>Historique annuel</mat-panel-title>
</mat-expansion-panel-header>
<app-history-table [headers]="seriesHeader" [names]="seriesName" [values]="seriesValue" [rows]="seriesRow"
[colors]="seriesColorClass"></app-history-table>
<!--
<div fxLayout="row wrap">
<div fxLayout="row wrap">
<h4 class="{{seriesColor[i]}} m-0" *ngFor='let name of seriesName; let i=index' fxFlex.gt-lg="25" fxFlex.gt-md="50" fxFlex.gt-sm="50" fxFlex.gt-xs="25" fxFlex="100">
<i class="mdi mdi-checkbox-blank-circle font-10 m-r-10 "></i>{{name}} ft² : {{seriesValue[i]}}
</h4>
</div>
</div>
-->
</mat-expansion-panel>
</mat-accordion>
</mat-card>
@@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { CanopiesSizesComponent } from './canopies-sizes.component';
describe('CanopiesSizesComponent', () => {
let component: CanopiesSizesComponent;
let fixture: ComponentFixture<CanopiesSizesComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [CanopiesSizesComponent]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(CanopiesSizesComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,92 @@
import { Component, DestroyRef, inject, OnInit, OnDestroy } from '@angular/core';
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
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 { PieChartComponent } from 'src/app/components/shared/helpers-chart';
import { HistoryTableComponent } from 'src/app/components/shared/helpers-jump';
import { UtilitiesService, CanopiesService } from 'src/app/core/services';
import { CanopyBySize, CanopyByYear } from 'src/app/core/models';
@Component({
selector: 'app-canopies-sizes',
standalone: true,
imports: [
MatButtonModule, MatCardModule, MatDividerModule,
MatExpansionModule, MatIconModule, MatMenuModule,
PieChartComponent, HistoryTableComponent
],
templateUrl: './canopies-sizes.component.html'
})
export class CanopiesSizesComponent implements OnInit, OnDestroy {
private _canopyBySize: Subscription = new Subscription();
private _canopyByYear: Subscription = new Subscription();
private _canopiesBySize!: Array<CanopyBySize>;
private _canopiesByYear!: Array<CanopyByYear>;
public title: string = 'Tailles de voile';
public subtitle: string = 'Nombre total de sauts par taille';
public seriesHeader: string[] = [];
public seriesName: string[] = [];
public seriesValue: number[] = [];
public seriesRow: Array<Array<number>> = [];
public seriesColor: {
backgroundColor: string[],
borderColor: string[]
} = {
backgroundColor: this._utilitiesService.getSeriesColors(0.8, 'all'),
borderColor: this._utilitiesService.getSeriesColors(1, 'all')
};
public seriesColorClass: string[] = this._utilitiesService.getChartColors();
public destroyRef = inject(DestroyRef);
constructor(
private _canopiesService: CanopiesService,
private _utilitiesService: UtilitiesService,
public menuItems: MenuItems
) { }
ngOnInit() {
this._loadCanopyBySize();
}
ngOnDestroy() {
this._canopyBySize.unsubscribe();
this._canopyByYear.unsubscribe();
}
private _loadCanopyBySize(): void {
const canopies$: Observable<Array<CanopyBySize>> = this._canopiesService.getAllBySize().pipe(takeUntilDestroyed(this.destroyRef));
this._canopyBySize = canopies$.subscribe((aggregate: Array<CanopyBySize>) => {
this._canopiesBySize = aggregate.map((row: CanopyBySize) => {
this.seriesName.push(row.taille.toString());
this.seriesValue.push(row.count);
return row;
});
this._loadCanopyByYear();
});
}
private _loadCanopyByYear(): void {
this.seriesHeader = ['2018', '2019', '2020', '2021', '2022', '2023', '2024'];
const values: Array<number> = this.seriesHeader.map(() => 0);
this.seriesName.forEach(() => {
this.seriesRow.push([...values]);
});
const dropzones$: Observable<Array<CanopyByYear>> = this._canopiesService.getAllBySizeByYear().pipe(takeUntilDestroyed(this.destroyRef));
this._canopyByYear = dropzones$.subscribe((aggregate: Array<CanopyByYear>) => {
this._canopiesByYear = aggregate.map((row: CanopyByYear) => {
const taille: string = row.taille.toString();
const year: string = row.year.toString();
this.seriesRow[this.seriesName.indexOf(taille)][this.seriesHeader.indexOf(year)] = row.count;
return row;
});
});
}
}
@@ -0,0 +1,18 @@
{
"Bar": {
"labels": ["Janvier", "Février", "Mars", "Avril", "Mai", "Juin", "Juillet", "Août", "Septembre", "Octobre", "Novembre", "Décembre"],
"series": [
[0, 0, 0, 0, 0, 0, 0, 7, 0, 0, 0, 0],
[0, 0, 0, 0, 7, 1, 17, 22, 12, 18, 0, 0],
[0, 35, 8, 0, 0, 10, 31, 67, 26, 24, 0, 0],
[0, 0, 0, 0, 0, 9, 8, 0, 11, 24, 17, 4],
[0, 0, 0, 29, 37, 11, 22, 20, 17, 13, 13, 0],
[0, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
]
},
"Pie": {
"labels": ["261", "45", "178", "16", "13", "11", "4", "2"],
"series": [261, 45, 178, 16, 13, 11, 4, 2],
"names": ["Arcachon", "Béni-Mellal", "La Réole", "Royan", "Pamiers", "Soulac", "Sables d'Olonne", "Rochefort"]
}
}
@@ -0,0 +1,35 @@
<mat-card>
<mat-card-header>
<mat-card-title>{{ title }}</mat-card-title>
<mat-card-subtitle>{{ subtitle }}</mat-card-subtitle>
<span class="flex-spacer"></span>
<button mat-icon-button [matMenuTriggerFor]="menuDropzoneBar" aria-label="Menu Dropzone Bar">
<mat-icon fontIcon="more_vert"></mat-icon>
</button>
<mat-menu #menuDropzoneBar="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>
</mat-card-header>
<mat-divider class="my-3"></mat-divider>
<mat-card-content>
@if (displayCharts) {
<div class="barchart position-relative w-100 my-1">
<x-chartist [configuration]="barChartDropZones"></x-chartist>
</div>
}
</mat-card-content>
<mat-divider class="mt-3 mb-0"></mat-divider>
<mat-accordion>
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title>Historique annuel</mat-panel-title>
</mat-expansion-panel-header>
<app-history-table [headers]="seriesHeader" [names]="seriesName" [values]="seriesValue" [rows]="seriesRow" [colors]="seriesColorClass"></app-history-table>
</mat-expansion-panel>
</mat-accordion>
</mat-card>
@@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { DropzonesBarComponent } from './dropzones-bar.component';
describe('DropzonesBarComponent', () => {
let component: DropzonesBarComponent;
let fixture: ComponentFixture<DropzonesBarComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [DropzonesBarComponent]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(DropzonesBarComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,143 @@
import { Component, DestroyRef, inject, OnInit, OnDestroy } from '@angular/core';
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
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 { BaseChartDirective } from 'ng2-charts';
import { ChartistModule, Configuration } from 'ng-chartist';
import { AxisOptions, Label } from 'chartist';
import { Observable, Subscription } from 'rxjs';
import { MenuItems } from 'src/app/components/shared';
import { HistoryTableComponent } from 'src/app/components/shared/helpers-jump';
import { UtilitiesService, DropZonesService } from 'src/app/core/services';
import { DropZoneByOaci, DropZoneByYear } from 'src/app/core/models';
@Component({
selector: 'app-dropzones-bar',
standalone: true,
imports: [
MatButtonModule, MatCardModule, MatDividerModule,
MatExpansionModule, MatIconModule, MatMenuModule,
BaseChartDirective, ChartistModule,
HistoryTableComponent
],
templateUrl: './dropzones-bar.component.html'
})
export class DropzonesBarComponent implements OnInit, OnDestroy {
private _dropzoneByOaci: Subscription = new Subscription();
private _dropzoneByYear: Subscription = new Subscription();
private _dropzonesByOaci!: Array<DropZoneByOaci>;
private _dropzonesByYear!: Array<DropZoneByYear>;
public title: string = 'Les dropzones';
public subtitle: string = 'Nombre total de sauts par dropzone';
public seriesHeader: string[] = [];
public seriesName: string[] = [];
public seriesValue: number[] = [];
public seriesRow: Array<Array<number>> = [];
public seriesColor: {
backgroundColor: string[],
borderColor: string[]
} = {
backgroundColor: this._utilitiesService.getSeriesColors(0.8, 'all'),
borderColor: this._utilitiesService.getSeriesColors(1, 'all')
};
public seriesColorClass: string[] = this._utilitiesService.getChartColors();
public displayCharts = false;
public barChartDropZones: Configuration = this._utilitiesService.getBarConfig();
public destroyRef = inject(DestroyRef);
constructor(
private _dropzonesService: DropZonesService,
private _utilitiesService: UtilitiesService,
public menuItems: MenuItems
) { }
ngOnInit() {
this._loadDropZoneByOaci();
}
ngOnDestroy() {
this._dropzoneByOaci.unsubscribe();
this._dropzoneByYear.unsubscribe();
}
private _loadDropZoneByOaci(): void {
const dropzones$: Observable<Array<DropZoneByOaci>> = this._dropzonesService.getAllByOaci().pipe(takeUntilDestroyed(this.destroyRef));
this._dropzoneByOaci = dropzones$.subscribe((aggregate: Array<DropZoneByOaci>) => {
this._dropzonesByOaci = aggregate.map((row: DropZoneByOaci) => {
this.seriesName.push(`${row.oaci} - ${row.lieu}`);
this.seriesValue.push(row.count);
return row;
});
this._loadDropZoneByYear();
});
}
private _loadDropZoneByYear(): void {
this.seriesHeader = ['2018', '2019', '2020', '2021', '2022', '2023', '2024'];
const values: Array<number> = this.seriesHeader.map(() => 0);
this.seriesName.forEach(() => {
this.seriesRow.push([...values]);
});
const dropzones$: Observable<Array<DropZoneByYear>> = this._dropzonesService.getAllByOaciByYear().pipe(takeUntilDestroyed(this.destroyRef));
this._dropzoneByYear = dropzones$.subscribe((aggregate: Array<DropZoneByYear>) => {
this._dropzonesByYear = aggregate.map((row: DropZoneByYear) => {
const lieu: string = row.lieu;
const oaci: string = row.oaci;
const year: string = row.year.toString();
this.seriesRow[this.seriesName.indexOf(`${oaci} - ${lieu}`)][this.seriesHeader.indexOf(year)] = row.count;
return row;
});
const axisX: AxisOptions = {
labelInterpolationFnc: function(value: Label, index: number): string {
return index % 1 === 0 ? `${value.toString()}` : '';
}
}
this.barChartDropZones = {
type: 'Bar',
data: {
'labels': this.seriesHeader,
'series': this.seriesRow
},
options: {
seriesBarDistance: 15,
horizontalBars: false,
high: 180,
axisX: {
showGrid: false,
offset: 20
},
axisY: {
showGrid: true,
offset: 40
},
height: 300
},
responsiveOptions: [
['screen and (min-width: 1024px)', {
axisX: axisX
}],
['screen and (min-width: 641px) and (max-width: 1024px)', {
seriesBarDistance: 7,
axisY: {
offset: 30
},
axisX: axisX
}],
['screen and (max-width: 640px)', {
seriesBarDistance: 5,
axisY: {
offset: 20
},
axisX: axisX
}]
]
};
this.displayCharts = true;
});
}
}
@@ -0,0 +1,18 @@
{
"Bar": {
"labels": ["Janvier", "Février", "Mars", "Avril", "Mai", "Juin", "Juillet", "Août", "Septembre", "Octobre", "Novembre", "Décembre"],
"series": [
[0, 0, 0, 0, 0, 0, 0, 7, 0, 0, 0, 0],
[0, 0, 0, 0, 7, 1, 17, 22, 12, 18, 0, 0],
[0, 35, 8, 0, 0, 10, 31, 67, 26, 24, 0, 0],
[0, 0, 0, 0, 0, 9, 8, 0, 11, 24, 17, 4],
[0, 0, 0, 29, 37, 11, 22, 20, 17, 13, 13, 0],
[0, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
]
},
"Pie": {
"labels": ["261", "45", "178", "16", "13", "11", "4", "2"],
"series": [261, 45, 178, 16, 13, 11, 4, 2],
"names": ["Arcachon", "Béni-Mellal", "La Réole", "Royan", "Pamiers", "Soulac", "Sables d'Olonne", "Rochefort"]
}
}
@@ -0,0 +1,31 @@
<mat-card>
<mat-card-header>
<mat-card-title>{{ title }}</mat-card-title>
<mat-card-subtitle>{{ subtitle }}</mat-card-subtitle>
<span class="flex-spacer"></span>
<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>
</mat-card-header>
<mat-divider class="my-3"></mat-divider>
<mat-card-content>
<app-pie-chart [headers]="seriesHeader" [names]="seriesName" [values]="seriesValue" [colors]="seriesColor"></app-pie-chart>
</mat-card-content>
<mat-divider class="mt-3 mb-0"></mat-divider>
<mat-accordion>
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title>Historique annuel</mat-panel-title>
</mat-expansion-panel-header>
<app-history-table [headers]="seriesHeader" [names]="seriesName" [values]="seriesValue" [rows]="seriesRow" [colors]="seriesColorClass"></app-history-table>
</mat-expansion-panel>
</mat-accordion>
</mat-card>
@@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { DropzonesPieComponent } from './dropzones-pie.component';
describe('DropzonesPieComponent', () => {
let component: DropzonesPieComponent;
let fixture: ComponentFixture<DropzonesPieComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [DropzonesPieComponent]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(DropzonesPieComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,93 @@
import { Component, DestroyRef, inject, OnInit, OnDestroy } from '@angular/core';
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
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 { ChartConfiguration } from 'chart.js';
import { Observable, Subscription } from 'rxjs';
import { MenuItems } from 'src/app/components/shared';
import { PieChartComponent } from 'src/app/components/shared/helpers-chart';
import { HistoryTableComponent } from 'src/app/components/shared/helpers-jump';
import { UtilitiesService, DropZonesService } from 'src/app/core/services';
import { DropZoneByOaci, DropZoneByYear } from 'src/app/core/models';
@Component({
selector: 'app-dropzones-pie',
standalone: true,
imports: [
MatButtonModule, MatCardModule, MatDividerModule,
MatExpansionModule, MatIconModule, MatMenuModule,
PieChartComponent, HistoryTableComponent
],
templateUrl: './dropzones-pie.component.html'
})
export class DropzonesPieComponent implements OnInit, OnDestroy {
private _dropzoneByOaci: Subscription = new Subscription();
private _dropzoneByYear: Subscription = new Subscription();
private _dropzonesByOaci!: Array<DropZoneByOaci>;
private _dropzonesByYear!: Array<DropZoneByYear>;
public title: string = 'Les dropzones';
public subtitle: string = 'Nombre total de sauts par dropzone';
public seriesHeader: string[] = [];
public seriesName: string[] = [];
public seriesValue: number[] = [];
public seriesRow: Array<Array<number>> = [];
public seriesColor: {
backgroundColor: string[],
borderColor: string[]
} = {
backgroundColor: this._utilitiesService.getSeriesColors(0.8, 'all'),
borderColor: this._utilitiesService.getSeriesColors(1, 'all')
};
public seriesColorClass: string[] = this._utilitiesService.getChartColors();
public destroyRef = inject(DestroyRef);
constructor(
private _dropzonesService: DropZonesService,
private _utilitiesService: UtilitiesService,
public menuItems: MenuItems
) { }
ngOnInit() {
this._loadDropZoneByOaci();
}
ngOnDestroy() {
this._dropzoneByOaci.unsubscribe();
this._dropzoneByYear.unsubscribe();
}
private _loadDropZoneByOaci(): void {
const dropzones$: Observable<Array<DropZoneByOaci>> = this._dropzonesService.getAllByOaci().pipe(takeUntilDestroyed(this.destroyRef));
this._dropzoneByOaci = dropzones$.subscribe((aggregate: Array<DropZoneByOaci>) => {
this._dropzonesByOaci = aggregate.map((row: DropZoneByOaci) => {
this.seriesName.push(`${row.oaci} - ${row.lieu}`);
this.seriesValue.push(row.count);
return row;
});
this._loadDropZoneByYear();
});
}
private _loadDropZoneByYear(): void {
this.seriesHeader = ['2018', '2019', '2020', '2021', '2022', '2023', '2024'];
const values: Array<number> = this.seriesHeader.map(() => 0);
this.seriesName.forEach(() => {
this.seriesRow.push([...values]);
});
const dropzones$: Observable<Array<DropZoneByYear>> = this._dropzonesService.getAllByOaciByYear().pipe(takeUntilDestroyed(this.destroyRef));
this._dropzoneByYear = dropzones$.subscribe((aggregate: Array<DropZoneByYear>) => {
this._dropzonesByYear = aggregate.map((row: DropZoneByYear) => {
const lieu: string = row.lieu;
const oaci: string = row.oaci;
const year: string = row.year.toString();
this.seriesRow[this.seriesName.indexOf(`${oaci} - ${lieu}`)][this.seriesHeader.indexOf(year)] = row.count;
return row;
});
});
}
}
@@ -0,0 +1,7 @@
export * from './aeronefs-pie/aeronefs-pie.component';
export * from './aeronefs-bar/aeronefs-bar.component';
export * from './canopies-models/canopies-models.component';
export * from './canopies-sizes/canopies-sizes.component';
export * from './dropzones-pie/dropzones-pie.component';
export * from './dropzones-bar/dropzones-bar.component';
export * from './jumps-by-month/jumps-by-month.component';
@@ -0,0 +1,17 @@
{
"Bar": {
"labels": ["Janvier", "Février", "Mars", "Avril", "Mai", "Juin", "Juillet", "Août", "Septembre", "Octobre", "Novembre", "Décembre"],
"series": [
[0, 0, 0, 0, 0, 0, 0, 7, 0, 0, 0, 0],
[0, 0, 0, 0, 7, 1, 17, 22, 12, 18, 0, 0],
[0, 35, 8, 0, 0, 10, 31, 67, 26, 24, 0, 0],
[0, 0, 0, 0, 0, 9, 8, 0, 11, 24, 17, 4],
[0, 0, 0, 29, 37, 11, 22, 20, 17, 13, 13, 0],
[0, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
]
},
"Pie": {
"labels": ["Arcachon", "Béni-Mellal", "La Réole", "Royan", "Pamiers", "Soulac", "Sables d'Olonne", "Rochefort"],
"series": [261, 45, 178, 16, 13, 11, 4, 2]
}
}
@@ -0,0 +1,86 @@
<mat-card>
<mat-card-header>
<mat-card-title>{{ title }}</mat-card-title>
<mat-card-subtitle>{{ subtitle }} de {{min}} à {{max}}</mat-card-subtitle>
<span class="flex-spacer"></span>
<button mat-icon-button [matMenuTriggerFor]="menuJumpBar" aria-label="Menu Dropzone Bar">
<mat-icon fontIcon="more_vert"></mat-icon>
</button>
<mat-menu #menuJumpBar="matMenu">
<button mat-menu-item *ngFor="let menuitem of menuItems.getMenuPanel()">
<mat-icon [fontIcon]="menuitem.icon"></mat-icon>
<span>{{ menuitem.name }}</span>
</button>
</mat-menu>
</mat-card-header>
<mat-divider class="my-3"></mat-divider>
<mat-card-content>
<!--
<div class="text-center mb-3">
<ul class="list-inline my-0">
<li *ngFor='let name of seriesName; let i=index' class="list-inline-item me-lg-3 me-sm-2">
<span class="{{seriesColor[i]}} fs-6 m-0">
● {{name}}
</span>
</li>
</ul>
</div>
-->
<div *ngIf="displayCharts" class="barchart position-relative w-100 my-1">
<x-chartist [configuration]="barChartJumps"></x-chartist>
</div>
</mat-card-content>
<mat-divider class="mt-3 mb-0"></mat-divider>
<mat-accordion>
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title>Historique annuel</mat-panel-title>
</mat-expansion-panel-header>
<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>
</mat-expansion-panel>
</mat-accordion>
</mat-card>
@@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { JumpsByMonthComponent } from './jumps-by-month.component';
describe('JumpsByMonthComponent', () => {
let component: JumpsByMonthComponent;
let fixture: ComponentFixture<JumpsByMonthComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [JumpsByMonthComponent]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(JumpsByMonthComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,172 @@
import { Component, DestroyRef, inject, OnInit, OnDestroy } from '@angular/core';
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
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 { ChartistModule, Configuration } from 'ng-chartist';
import { AxisOptions, Label } from 'chartist';
import { Observable, Subscription } from 'rxjs';
import { MenuItems } from 'src/app/components/shared';
import { UtilitiesService, JumpsService } from 'src/app/core/services';
import { JumpByDate } from 'src/app/core/models';
@Component({
selector: 'app-jumps-by-month',
standalone: true,
imports: [
CommonModule,
MatButtonModule, MatCardModule, MatDividerModule,
MatExpansionModule, MatIconModule, MatMenuModule,
ChartistModule
],
templateUrl: './jumps-by-month.component.html'
})
export class JumpsByMonthComponent implements OnInit, OnDestroy {
private _jumpByDate: Subscription = new Subscription();
private _jumpsByDate!: Array<JumpByDate>;
private _jumpsByDateCount = 0;
public title: string = 'Volume mensuel';
public subtitle: string = 'Nombre total de sauts par mois';
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 seriesColorClass: string[] = this._utilitiesService.getChartColors();
public seriesRowTotal: number[] = [];
public seriesRowAvg: number[] = [];
public seriesRowAvgLastYears: number[] = [];
public seriesColTotal: number[] = [];
public seriesColTotalClosed: number[] = [];
public seriesColTotalLastYears: number[] = [];
public displayCharts = false;
public barChartJumps: Configuration = this._utilitiesService.getBarConfig();
public destroyRef = inject(DestroyRef);
constructor(
private _jumpsService: JumpsService,
private _utilitiesService: UtilitiesService,
public menuItems: MenuItems
) { }
ngOnInit() {
this._loadJumpByDate();
}
ngOnDestroy() {
this._jumpByDate.unsubscribe();
}
private _loadJumpByDate() {
const minoration = 2; // -2 pour ne pas compter la première année
this.seriesHeader = this._utilitiesService.getMonthsList();
const values: Array<number> = this.seriesHeader.map(() => 0);
this.seriesColTotal = [...values];
this.seriesColTotalClosed = [...values];
this.seriesColTotalLastYears = [...values];
const jumps$: Observable<Array<JumpByDate>> = this._jumpsService.getAllByDate().pipe(takeUntilDestroyed(this.destroyRef));
this._jumpByDate = jumps$.subscribe((aggregate: Array<JumpByDate>) => {
this._jumpsByDateCount = aggregate.length;
this._jumpsByDate = aggregate.map((row: JumpByDate) => {
const currentYear: number = new Date().getFullYear();
const year = row.year.toString();
const month = row.month;
if (this.seriesName.indexOf(year) < 0) {
this.seriesName.push(year);
if (row.year < this.min || this.min == 0) {
this.min = row.year;
}
if (row.year > this.max) {
this.max = row.year;
}
this.seriesRow.push([...values]);
}
this.seriesRow[(this.seriesRow.length-1)][(month-1)] = row.count;
this.seriesColTotal[(month-1)] += row.count;
if (row.year < currentYear) {
this.seriesColTotalClosed[(month-1)] += row.count;
}
if (row.year >= (currentYear-3) && row.year < currentYear) {
this.seriesColTotalLastYears[(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);
const axisX: AxisOptions = {
labelInterpolationFnc: function(value: Label, index: number): string {
return index % 1 === 0 ? `${value.toString()}` : '';
}
}
this.barChartJumps = {
type: 'Bar',
data: {
'labels': this.seriesHeader,
'series': this.seriesRow
},
options: {
seriesBarDistance: 15,
high: 70,
axisX: {
showGrid: false,
offset: 20
},
axisY: {
showGrid: true,
offset: 40
},
height: 300
},
responsiveOptions: [
['screen and (min-width: 1024px)', {
axisX: axisX
}],
['screen and (min-width: 641px) and (max-width: 1024px)', {
seriesBarDistance: 10,
axisY: {
offset: 30
},
axisX: axisX
}],
['screen and (max-width: 640px)', {
seriesBarDistance: 5,
axisY: {
offset: 20
},
axisX: axisX
}]
]
};
this.displayCharts = true;
});
}
}
@@ -0,0 +1,24 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { FrenchPaginator } from './french-paginator.component';
describe('FrenchPaginator', () => {
let component: FrenchPaginator;
let fixture: ComponentFixture<FrenchPaginator>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [FrenchPaginator]
})
.compileComponents();
fixture = TestBed.createComponent(FrenchPaginator);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,21 @@
import { MatPaginatorIntl } from '@angular/material/paginator';
import { Subject } from 'rxjs';
export class FrenchPaginator implements MatPaginatorIntl {
changes = new Subject<void>();
// For internationalization, the `$localize` function from
// the `@angular/localize` package can be used.
firstPageLabel = `Première page`;
itemsPerPageLabel = `Eléments par page :`;
lastPageLabel = `Dernière page`;
nextPageLabel = 'Page suivante';
previousPageLabel = 'Page précédente';
getRangeLabel(page: number, pageSize: number, length: number): string {
if (length === 0) {
return `Page 1 sur 1`;
}
const amountPages = Math.ceil(length / pageSize);
return `Page ${page + 1} sur ${amountPages}`;
}
}
@@ -0,0 +1,11 @@
@if (displayCharts) {
<div class="barchart position-relative w-100 my-1">
<canvas baseChart class="w-100 h-auto"
[data]="chartConfig.barChartData"
[options]="chartConfig.barChartOptions"
[plugins]="chartConfig.barChartPlugins!"
[legend]="chartConfig.barChartLegend"
[type]="'bar'">
</canvas>
</div>
}
@@ -0,0 +1,25 @@
import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing';
import { BarChartComponent } from './bar-chart.component';
describe('BarChartComponent', () => {
let component: BarChartComponent;
let fixture: ComponentFixture<BarChartComponent>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
imports: [BarChartComponent]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(BarChartComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,58 @@
import { Component, Input, OnChanges } from '@angular/core';
import { ChartDataset } from 'chart.js';
import { BaseChartDirective } from 'ng2-charts';
import { UtilitiesService } from 'src/app/core/services';
import { BarConfig } from 'src/app/core/models';
@Component({
selector: 'app-bar-chart',
standalone: true,
imports: [
BaseChartDirective
],
templateUrl: './bar-chart.component.html'
})
export class BarChartComponent implements OnChanges {
public displayCharts = false;
public chartConfig: BarConfig = this._utilitiesService.getBarChartConfig();
constructor(
private _utilitiesService: UtilitiesService
) { }
@Input() names: string[] = [];
@Input() values: number[] = [];
@Input() headers: string[] = [];
@Input() colors: {
backgroundColor: string[],
borderColor: string[]
} = {
backgroundColor: this._utilitiesService.getSeriesColors(0.8),
borderColor: this._utilitiesService.getSeriesColors(1)
};
@Input() label: string = 'Nombre total de sauts';
@Input() legend: boolean = false;
ngOnChanges() {
this._loadBarChart();
}
private _loadBarChart(): void {
const dataset: ChartDataset<'bar'> = {
data: [...this.values],
label: this.label,
backgroundColor: this.colors.backgroundColor,
borderColor: this.colors.borderColor,
borderWidth: 1
};
this.chartConfig.barChartData.labels = [...this.names];
this.chartConfig.barChartData.datasets!.push(dataset);
/*this.chartConfig.barChartOptions!.scales = {
x: {display: false},
y: {display: true}
};*/
this.chartConfig.barChartLegend = this.legend;
this.displayCharts = true;
}
}
@@ -0,0 +1,11 @@
@if (displayCharts) {
<div class="barchart position-relative w-100 my-1">
<canvas baseChart
[data]="chartConfig.barChartData"
[options]="chartConfig.barChartOptions"
[plugins]="chartConfig.barChartPlugins!"
[legend]="chartConfig.barChartLegend"
[type]="'bar'">
</canvas>
</div>
}
@@ -0,0 +1,25 @@
import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing';
import { BarHorizontalChartComponent } from './bar-horizontal-chart.component';
describe('BarHorizontalChartComponent', () => {
let component: BarHorizontalChartComponent;
let fixture: ComponentFixture<BarHorizontalChartComponent>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
imports: [BarHorizontalChartComponent]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(BarHorizontalChartComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,54 @@
import { Component, Input, OnChanges } from '@angular/core';
import { ChartDataset } from 'chart.js';
import { BaseChartDirective } from 'ng2-charts';
import { UtilitiesService } from 'src/app/core/services';
import { BarConfig } from 'src/app/core/models';
@Component({
selector: 'app-bar-horizontal-chart',
standalone: true,
imports: [
BaseChartDirective
],
templateUrl: './bar-horizontal-chart.component.html'
})
export class BarHorizontalChartComponent implements OnChanges {
public displayCharts = false;
public chartConfig: BarConfig = this._utilitiesService.getHorizontalBarChartConfig();
constructor(
private _utilitiesService: UtilitiesService
) { }
@Input() names: string[] = [];
@Input() values: number[] = [];
@Input() headers: string[] = [];
@Input() colors: {
backgroundColor: string[],
borderColor: string[]
} = {
backgroundColor: this._utilitiesService.getSeriesColors(0.8),
borderColor: this._utilitiesService.getSeriesColors(1)
};
@Input() label: string = 'Nombre total de sauts';
@Input() legend: boolean = false;
ngOnChanges() {
this._loadBarChart();
}
private _loadBarChart(): void {
const dataset: ChartDataset<'bar'> = {
data: [...this.values],
label: this.label,
backgroundColor: this.colors.backgroundColor,
borderColor: this.colors.borderColor,
borderWidth: 1
};
this.chartConfig.barChartData.labels = [...this.names];
this.chartConfig.barChartData.datasets!.push(dataset);
this.chartConfig.barChartLegend = this.legend;
this.displayCharts = true;
}
}
@@ -0,0 +1,11 @@
@if (displayCharts) {
<div class="barchart position-relative w-100 my-3">
<canvas baseChart class="w-100 h-auto"
[data]="chartConfig.barChartData"
[options]="chartConfig.barChartOptions"
[plugins]="chartConfig.barChartPlugins!"
[legend]="chartConfig.barChartLegend"
[type]="'bar'">
</canvas>
</div>
}
@@ -0,0 +1,25 @@
import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing';
import { BarsChartComponent } from './bars-chart.component';
describe('BarsChartComponent', () => {
let component: BarsChartComponent;
let fixture: ComponentFixture<BarsChartComponent>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
imports: [BarsChartComponent]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(BarsChartComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,79 @@
import { Component, Input, OnChanges, AfterContentChecked } from '@angular/core';
import { ChartDataset } from 'chart.js';
import { BaseChartDirective } from 'ng2-charts';
import { UtilitiesService } from 'src/app/core/services';
import { BarConfig } from 'src/app/core/models';
@Component({
selector: 'app-bars-chart',
standalone: true,
imports: [
BaseChartDirective
],
templateUrl: './bars-chart.component.html'
})
export class BarsChartComponent implements OnChanges, AfterContentChecked {
public displayCharts = false;
public chartConfig: BarConfig = this._utilitiesService.getBarChartConfig();
constructor(
private _utilitiesService: UtilitiesService
) { }
@Input() names: string[] = [];
@Input() values: Array<Array<number>> = [];
@Input() headers: string[] = [];
@Input() colors: {
backgroundColor: string[],
borderColor: string[]
} = {
backgroundColor: this._utilitiesService.getSeriesColors(0.8),
borderColor: this._utilitiesService.getSeriesColors(1)
};
@Input() legend: boolean = false;
ngOnChanges() {
this._loadBarChart();
}
ngAfterContentChecked() {
this._loadBarChart();
}
private _loadBarChart(): void {
this.chartConfig = this._utilitiesService.getBarChartConfig();
this.chartConfig.barChartData.labels = [...this.headers];
this.chartConfig.barChartOptions!.maintainAspectRatio = false;
//this.chartConfig.barChartOptions!.aspectRatio = 2.4;
this.chartConfig.barChartOptions!.scales = {
x: {
display: true,
//stacked: true,
beginAtZero: true,
ticks: { color: 'rgba(255,255,255,0.3)' },
grid: { color: 'rgba(255,255,255,0.1)', display: true, tickBorderDash: [1,2], tickBorderDashOffset: 2 }
},
y: {
display: true,
//stacked: true,
beginAtZero: true,
ticks: { color: 'rgba(255,255,255,0.3)' },
grid: { color: 'rgba(255,255,255,0.2)', tickBorderDash: [1,2], tickBorderDashOffset: 2, display: true }
}
};
this.names.forEach((label: string, index: number) => {
const dataset: ChartDataset<'bar'> = {
data: [...this.values[index]],
label: label,
backgroundColor: this.colors.backgroundColor[index],
borderColor: this.colors.borderColor[index],
borderWidth: 1,
//stack: 'Stack 0'
};
this.chartConfig.barChartData.datasets!.push(dataset);
});
this.chartConfig.barChartLegend = this.legend;
this.displayCharts = true;
}
}
@@ -0,0 +1,11 @@
@if (displayCharts) {
<div class="circlechart position-relative w-100 my-3">
<canvas baseChart class="w-100 h-auto"
[labels]="chartConfig.circleChartLabels"
[datasets]="chartConfig.circleChartDatasets"
[options]="chartConfig.circleChartOptions"
[legend]="chartConfig.circleChartLegend"
[type]="'doughnut'">
</canvas>
</div>
}
@@ -0,0 +1,25 @@
import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing';
import { CircleChartComponent } from './circle-chart.component';
describe('CircleChartComponent', () => {
let component: CircleChartComponent;
let fixture: ComponentFixture<CircleChartComponent>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
imports: [CircleChartComponent]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(CircleChartComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,64 @@
import { Component, Input, OnChanges } from '@angular/core';
//import { ChartDataset } from 'chart.js';
import { BaseChartDirective } from 'ng2-charts';
import { UtilitiesService } from 'src/app/core/services';
import { CircleConfig } from 'src/app/core/models';
@Component({
selector: 'app-circle-chart',
standalone: true,
imports: [
BaseChartDirective
],
templateUrl: './circle-chart.component.html'
})
export class CircleChartComponent implements OnChanges {
public displayCharts = false;
public chartConfig: CircleConfig = this._utilitiesService.getCircleChartConfig();
constructor(
private _utilitiesService: UtilitiesService
) { }
@Input() names: string[] = [];
@Input() values: number[] = [];
@Input() headers: string[] = [];
@Input() color: number = 0;
@Input() label: string = 'Nombre total de sauts';
@Input() legend: boolean = false;
ngOnChanges() {
this._loadCircleChart();
}
private _loadCircleChart(): void {
this.chartConfig.circleChartLabels = [...this.names];
this.chartConfig.circleChartDatasets[0].data = [...this.values];
this.chartConfig.circleChartDatasets[0].label = this.label;
this.chartConfig.circleChartDatasets[0].backgroundColor = [
this._utilitiesService.getSeriesColors(0.9, 'pastels')[this.color],
'rgba(255, 255, 255, 0.05)'
];
this.chartConfig.circleChartDatasets[0].borderColor = [
this._utilitiesService.getSeriesColors(1, 'pastels')[this.color],
'rgba(255, 255, 255, 0.05)'
];
this.chartConfig.circleChartDatasets[0].borderWidth = 0;
this.chartConfig.circleChartLegend = this.legend;
this.displayCharts = true;
}
/*private _loadCircleChart(): void {
const dataset: ChartDataset<'circle'> = {
data: [...this.values],
label: 'Nombre total de sauts',
backgroundColor: this._utilitiesService.getSeriesColors(0.8),
borderColor: this._utilitiesService.getSeriesColors(1),
borderWidth: 1
};
this.chartConfig.circleChartLabels = [...this.names];
this.chartConfig.circleChartDatasets!.push(dataset);
this.displayCharts = true;
}*/
}
@@ -0,0 +1,7 @@
export * from './bar-chart.component';
export * from './bar-horizontal-chart.component';
export * from './bars-chart.component';
export * from './circle-chart.component';
export * from './line-chart.component';
export * from './linearea-chart.component';
export * from './pie-chart.component';
@@ -0,0 +1,10 @@
@if(displayCharts) {
<div class="linechart position-relative w-100 my-3">
<canvas baseChart class="w-100 h-auto"
[data]="chartConfig.lineChartData"
[options]="chartConfig.lineChartOptions"
[legend]="chartConfig.lineChartLegend"
[type]="'line'">
</canvas>
</div>
}
@@ -0,0 +1,25 @@
import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing';
import { LineChartComponent } from './line-chart.component';
describe('LineChartComponent', () => {
let component: LineChartComponent;
let fixture: ComponentFixture<LineChartComponent>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
imports: [LineChartComponent]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(LineChartComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,64 @@
import { Component, Input, OnChanges } from '@angular/core';
import { ChartDataset, Point } from 'chart.js';
import { BaseChartDirective } from 'ng2-charts';
import { UtilitiesService } from 'src/app/core/services';
import { LineConfig } from 'src/app/core/models';
@Component({
selector: 'app-line-chart',
standalone: true,
imports: [
BaseChartDirective
],
templateUrl: './line-chart.component.html'
})
export class LineChartComponent implements OnChanges {
public displayCharts = false;
public chartConfig: LineConfig = this._utilitiesService.getLineChartConfig();
constructor(
private _utilitiesService: UtilitiesService
) { }
@Input() headers: string[] = [];
@Input() names: string[] = [];
@Input() values: Array<Array<number>> = [];
@Input() colors: {
backgroundColor: string[],
borderColor: string[]
} = {
backgroundColor: this._utilitiesService.getSeriesColors(0.8),
borderColor: this._utilitiesService.getSeriesColors(1)
};
@Input() legend: boolean = false;
@Input() hideZero: boolean = false;
ngOnChanges() {
this._loadLineChart();
}
private _loadLineChart(): void {
this.chartConfig.lineChartData.labels = [...this.headers];
this.names.forEach((label: string, index: number) => {
let data: Array<(number | Point | null)> = [];
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,
backgroundColor: this.colors.backgroundColor[index],
borderColor: this.colors.borderColor[index],
borderWidth: 2,
cubicInterpolationMode: 'monotone',
tension: 0
};
this.chartConfig.lineChartData.datasets!.push(dataset);
});
this.chartConfig.lineChartLegend = this.legend;
this.displayCharts = true;
}
}
@@ -0,0 +1,10 @@
@if(displayCharts) {
<div class="linechart position-relative w-100 my-3">
<canvas baseChart class="w-100 h-auto"
[data]="chartConfig.lineChartData"
[options]="chartConfig.lineChartOptions"
[legend]="chartConfig.lineChartLegend"
[type]="'line'">
</canvas>
</div>
}
@@ -0,0 +1,25 @@
import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing';
import { LineAreaChartComponent } from './linearea-chart.component';
describe('LineAreaChartComponent', () => {
let component: LineAreaChartComponent;
let fixture: ComponentFixture<LineAreaChartComponent>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
imports: [LineAreaChartComponent]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(LineAreaChartComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,63 @@
import { Component, Input, OnChanges } from '@angular/core';
import { ChartDataset } from 'chart.js';
import { BaseChartDirective } from 'ng2-charts';
import { UtilitiesService } from 'src/app/core/services';
import { LineConfig } from 'src/app/core/models';
@Component({
selector: 'app-linearea-chart',
standalone: true,
imports: [
BaseChartDirective
],
templateUrl: './linearea-chart.component.html'
})
export class LineAreaChartComponent implements OnChanges {
public displayCharts = false;
public chartConfig: LineConfig = this._utilitiesService.getStackedLineAreaChartConfig();
constructor(
private _utilitiesService: UtilitiesService
) { }
@Input() headers: string[] = [];
@Input() names: string[] = [];
@Input() values: Array<Array<number>> = [];
@Input() colors: {
backgroundColor: string[],
borderColor: string[]
} = {
backgroundColor: this._utilitiesService.getSeriesColors(0.8),
borderColor: this._utilitiesService.getSeriesColors(1)
};
@Input() legend: boolean = false;
ngOnChanges() {
this._loadLineChart();
}
private _loadLineChart(): void {
this.chartConfig.lineChartData.labels = [...this.headers];
this.names.forEach((label: string, index: number) => {
let fill: string | number | boolean = '-1';
if (index === 0) {
fill = true;
}
const dataset: ChartDataset<'line'> = {
data: [...this.values[index]],
label: label,
backgroundColor: this.colors.backgroundColor[index],
borderColor: this.colors.borderColor[index],
borderWidth: 1,
fill: fill,
//stepped: true,
cubicInterpolationMode: 'monotone',
tension: 0
};
this.chartConfig.lineChartData.datasets!.push(dataset);
});
this.chartConfig.lineChartLegend = this.legend;
this.displayCharts = true;
}
}
@@ -0,0 +1,11 @@
@if (displayCharts) {
<div class="piechart position-relative w-100 my-3">
<canvas baseChart class="w-100 h-auto"
[labels]="chartConfig.doughnutChartLabels"
[datasets]="chartConfig.doughnutChartDatasets"
[options]="chartConfig.doughnutChartOptions"
[legend]="chartConfig.doughnutChartLegend"
[type]="'doughnut'">
</canvas>
</div>
}
@@ -0,0 +1,25 @@
import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing';
import { PieChartComponent } from './pie-chart.component';
describe('PieChartComponent', () => {
let component: PieChartComponent;
let fixture: ComponentFixture<PieChartComponent>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
imports: [PieChartComponent]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(PieChartComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,63 @@
import { Component, Input, OnChanges } from '@angular/core';
//import { ChartDataset } from 'chart.js';
import { BaseChartDirective } from 'ng2-charts';
import { UtilitiesService } from 'src/app/core/services';
import { DoughnutConfig } from 'src/app/core/models';
@Component({
selector: 'app-pie-chart',
standalone: true,
imports: [
BaseChartDirective
],
templateUrl: './pie-chart.component.html'
})
export class PieChartComponent implements OnChanges {
public displayCharts = false;
public chartConfig: DoughnutConfig = this._utilitiesService.getDoughnutChartConfig();
constructor(
private _utilitiesService: UtilitiesService
) { }
@Input() names: string[] = [];
@Input() values: number[] = [];
@Input() headers: string[] = [];
@Input() colors: {
backgroundColor: string[],
borderColor: string[]
} = {
backgroundColor: this._utilitiesService.getSeriesColors(0.8, 'all'),
borderColor: this._utilitiesService.getSeriesColors(1, 'all')
};
@Input() legend: boolean = false;
ngOnChanges() {
this._loadDoughnutChart();
}
private _loadDoughnutChart(): void {
this.chartConfig.doughnutChartLabels = [...this.names];
this.chartConfig.doughnutChartDatasets[0].data = [...this.values];
this.chartConfig.doughnutChartDatasets[0].label = 'Nombre total de sauts';
this.chartConfig.doughnutChartDatasets[0].backgroundColor = this.colors.backgroundColor;
this.chartConfig.doughnutChartDatasets[0].borderColor = this.colors.borderColor;
this.chartConfig.doughnutChartDatasets[0].borderWidth = 2;
this.chartConfig.doughnutChartLegend = this.legend;
this.displayCharts = true;
}
/*private _loadDoughnutChart(): void {
const dataset: ChartDataset<'doughnut'> = {
data: [...this.values],
label: 'Nombre total de sauts',
backgroundColor: this._utilitiesService.getSeriesColors(0.8),
borderColor: this._utilitiesService.getSeriesColors(1),
borderWidth: 1
};
this.chartConfig.doughnutChartLabels = [...this.names];
this.chartConfig.doughnutChartDatasets!.push(dataset);
this.displayCharts = true;
}*/
}
@@ -0,0 +1,29 @@
<table class="table table-striped table-dark table-hover">
<thead>
<tr>
<th></th>
<th *ngFor='let name of headers; let i=index' class="text-end"> {{name}} </th>
<th class="text-end">Total</th>
</tr>
</thead>
<tbody>
<tr *ngFor='let row of rows; let i=index'>
<th class="{{colors[i]}} text-end"> ● {{ names[i] }} </th>
<td *ngFor='let value of row; let i=index' class="font-monospace text-end">
<span class="{{ value === 0 ? 'text-empty pe-1' : 'pe-1'}}">{{ value }}</span>
</td>
<th class="{{colors[i]}} font-monospace text-end">
<span class="{{ values[i] === 0 ? 'text-empty pe-1' : 'pe-1'}}">{{ values[i] }}</span>
</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 class="{{ value === 0 ? 'text-empty pe-1' : 'pe-1'}}">{{ value }}</span>
</th>
<th class="font-monospace text-end">{{ grandTotal }}</th>
</tr>
</tfoot>
</table>
@@ -0,0 +1,24 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { HistoryTableComponent } from './history-table.component';
describe('HistoryTableComponent', () => {
let component: HistoryTableComponent;
let fixture: ComponentFixture<HistoryTableComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [HistoryTableComponent]
})
.compileComponents();
fixture = TestBed.createComponent(HistoryTableComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,46 @@
import { Component, Input, AfterContentChecked } from '@angular/core';
import { NgIf, NgFor } from '@angular/common';
@Component({
standalone: true,
imports: [
NgIf, NgFor
],
selector: 'app-history-table',
templateUrl: './history-table.component.html',
styleUrls: []
})
export class HistoryTableComponent implements AfterContentChecked {
public seriesColTotal: number[] = [];
public grandTotal: number = 0;
constructor() {}
@Input() headers: string[] = [];
@Input() names: string[] = [];
@Input() values: number[] = [];
@Input() rows: Array<Array<number>> = [];
@Input() colors: string[] = [];
/*ngOnChanges() {
//this._loadHistoryTable();
//console.log('onChanges');
}*/
ngAfterContentChecked() {
if (this.rows !== undefined && this.rows.length) {
this._loadHistoryTable();
}
}
private _loadHistoryTable(): void {
const values: Array<number> = this.rows[0].map(() => 0);
this.seriesColTotal = [...values];
this.rows.forEach((row: number[], rowIndex: number) => {
row.forEach((value: number, index: number) => {
this.seriesColTotal[index] = (this.seriesColTotal[index] + this.rows[rowIndex][index]);
});
this.grandTotal = this.seriesColTotal.reduce((partialSum, a) => partialSum + a, 0);
});
}
}
@@ -0,0 +1,5 @@
export * from './history-table.component';
export * from './jump-list.component';
export * from './jump-meta.component';
export * from './jump-preview.component';
export * from './jump-table.component';
@@ -0,0 +1,22 @@
<div class="clearfix mt-4 mb-1 border-bottom">
<h1 class="float-md-start me-3">{{ title }} <span class="fs-4 text-accent" [hidden]="!jumpsCount">({{ jumpsCount }})</span></h1>
<mat-spinner class="float-md-start" [diameter]="32" [hidden]="!loading" color="accent"></mat-spinner>
<button [hidden]="!showAdd" mat-raised-button color="accent" [routerLink]="['/editor']" class="float-md-end">
<mat-icon aria-label="Ajouter" fontIcon="add_notes"></mat-icon> Ajouter
</button>
</div>
<span [hidden]="!jumps.length" class="small mt-2 me-2 fst-italic">Prix actualisés toutes les {{ (refresh / 1000) }} secondes.</span>
<span [hidden]="jumps.length || loading">Aucun produit à afficher pour le moment</span>
<span [hidden]="!loading">Chargement des produits en cours</span>
@for (jump of jumps; track jump) {
<app-jump-preview [jump]="jump" [isUser]="isUser" [showMeta]="showMeta"></app-jump-preview>
}
<nav [hidden]="loading || pages.length <= 1" class="clearfix mt-3">
<ul class="pagination float-end">
@for (pageNumber of pages; track pageNumber) {
<li class="page-item" [ngClass]="{'active': pageNumber === currentPage}">
<button class="page-link" (click)="setPageTo(pageNumber)">{{ pageNumber }}</button>
</li>
}
</ul>
</nav>
@@ -0,0 +1,96 @@
import { Component, OnInit, OnDestroy, Input } from '@angular/core';
import { NgClass, DatePipe } from '@angular/common';
import { RouterLink } from '@angular/router';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { Observable, Subscription } from 'rxjs';
import { Errors, Jump, JumpList, JumpListConfig } from 'src/app/core/models';
import { JumpsService } from 'src/app/core/services';
import { JumpPreviewComponent } from './jump-preview.component';
@Component({
selector: 'app-jump-list',
templateUrl: './jump-list.component.html',
standalone: true,
imports: [
NgClass,
DatePipe,
MatProgressSpinnerModule,
MatButtonModule,
RouterLink,
MatIconModule,
JumpPreviewComponent
]
})
export class JumpListComponent implements OnInit, OnDestroy {
private _jumps: Subscription = new Subscription();
errors: Errors = { errors: {} };
query!: JumpListConfig;
jumps: Array<Jump> = [];
jumpsCount = 0;
loading = false;
currentPage = 1;
pages: Array<number> = [1];
lastUpdate: Date = new Date();
constructor(
private jumpsService: JumpsService
) { }
@Input() limit = 0;
@Input() refresh = 30000;
@Input() title = '';
@Input() showAdd = false;
@Input() isUser = false;
@Input() showMeta = false;
@Input()
set config(config: JumpListConfig) {
if (config) {
this.query = config;
this.currentPage = 1;
this.loading = true;
}
}
ngOnInit() {
this.runQuery();
}
ngOnDestroy() {
this._jumps.unsubscribe();
}
setPageTo(pageNumber: number) {
this.currentPage = pageNumber;
this.runQuery();
}
runQuery() {
if (this.limit) {
this.query.filters.limit = this.limit;
this.query.filters.offset = (this.limit * (this.currentPage - 1));
}
const jumps$: Observable<JumpList> = this.jumpsService.query(this.query);
this._jumps = jumps$.subscribe({
next: (data: JumpList) => {
this.loading = false;
this.jumps = data.jumps;
this.jumpsCount = data.jumpsCount;
if (this.limit) {
this.pages = this.getTotalPages(this.jumpsCount, this.limit);
}
},
error: (err) => {
this.errors = err;
}
});
}
getTotalPages(count: number, limit: number) {
// Used from http://www.jstips.co/en/create-range-0...n-easily-using-one-line/
return Array.from(new Array(Math.ceil(count / limit)), (val, index) => index + 1);
}
}
@@ -0,0 +1,14 @@
<div class="jump-meta float-end">
<a [routerLink]="['/profile', jump.author.username]">
<img [src]="jump.author.image" [alt]="jump.author.username" />
</a>
<div class="info">
<a class="author" [routerLink]="['/profile', jump.author.username]">
{{ jump.author.username }}
</a>
<span class="date">
{{ jump.createdAt | date: 'longDate' }}
</span>
</div>
<ng-content></ng-content>
</div>
@@ -0,0 +1,29 @@
/* Jumps Meta */
.jump-meta {
display: block;
position: relative;
font-weight: 300;
img {
display: inline-block;
vertical-align: middle;
height: 32px;
width: 32px;
border-radius: 30px;
}
.info {
margin: 0 1rem 0 .4rem;
display: inline-block;
vertical-align: middle;
line-height: 1rem;
.author {
display: block;
font-weight: 500 !important;
margin-bottom: 0.1rem;
}
.date {
color: #bbb;
font-size: .8rem;
display: block;
}
}
}
@@ -0,0 +1,16 @@
import { Component, Input } from '@angular/core';
import { DatePipe } from '@angular/common';
import { RouterLink } from '@angular/router';
import { Jump } from 'src/app/core/models';
@Component({
selector: 'app-jump-meta',
standalone: true,
imports: [ RouterLink, DatePipe ],
styleUrl: './jump-meta.component.scss',
templateUrl: './jump-meta.component.html'
})
export class JumpMetaComponent {
@Input() jump!: Jump;
}
@@ -0,0 +1,44 @@
<mat-card appearance="outlined" class="mt-4">
<mat-card-header class="pt-2">
<mat-card-title-group>
<mat-card-title><a [routerLink]="['/jump', jump.slug]">#{{ jump.numero }}</a></mat-card-title>
<mat-card-subtitle>{{ jump.categorie }} / {{ jump.module }} </mat-card-subtitle>
</mat-card-title-group>
</mat-card-header>
<mat-card-content>
<mat-divider [class]="['mt-1', 'mb-2']"></mat-divider>
<app-jump-meta [hidden]="!showMeta" [jump]="jump" class="float-sm-end"></app-jump-meta>
<div class="row mt-4">
<div class="col-sm-6">
<dl class="row mb-2">
<dt class="col-lg-4 col-md-5 col-sm-6 fw-normal">Date</dt>
<dd class="col-lg-8 col-md-7 col-sm-6">{{ jump.date }}</dd>
<dt class="col-lg-4 col-md-5 col-sm-6 fw-normal">Lieu</dt>
<dd class="col-lg-8 col-md-7 col-sm-6">{{ jump.lieu }} {{ jump.oaci }}</dd>
<dt class="col-lg-4 col-md-5 col-sm-6 fw-normal">Voile</dt>
<dd class="col-lg-8 col-md-7 col-sm-6">{{ jump.voile }} {{ jump.taille }}</dd>
</dl>
</div>
<div class="col-sm-6">
<dl class="row mb-2">
<dt class="col-lg-4 col-md-5 col-sm-6 fw-normal">Hauteur</dt>
<dd class="col-lg-8 col-md-7 col-sm-6">{{ jump.hauteur }}</dd>
<dt class="col-lg-4 col-md-5 col-sm-6 fw-normal">Aeronef</dt>
<dd class="col-lg-8 col-md-7 col-sm-6">{{ jump.aeronef }} {{ jump.imat }}</dd>
<dt class="col-lg-4 col-md-5 col-sm-6 fw-normal">Type de saut</dt>
<dd class="col-lg-8 col-md-7 col-sm-6">{{ jump.categorie }} {{ jump.module }}</dd>
</dl>
</div>
<div class="col-xs-12">
<h5>Participants ({{ jump.participants }})</h5>
</div>
</div>
<mat-divider [class]="['mt-2']"></mat-divider>
</mat-card-content>
<mat-card-actions>
<button mat-button color="danger" [disabled]="isDeleting" (click)="deleteJump()">SUPPRIMER</button>
<span class="flex-spacer"></span>
<button mat-button [routerLink]="['/editor', jump.slug]">MODIFIER</button>
<button mat-button color="primary" [routerLink]="['/jump', jump.slug]">VOIR</button>
</mat-card-actions>
</mat-card>
@@ -0,0 +1,44 @@
/* Jumps preview */
.jump-preview {
border-top: 1px solid rgba(0, 0, 0, .1);
padding: 1.4rem 0 0.5rem;
.jump-meta {
margin: 0 0 0.2rem;
}
h3 .preview-link {
font-weight: 600 !important;
font-size: 1.5rem !important;
margin-bottom: 3px;
}
}
.preview-link {
color: inherit !important;
&:hover {
text-decoration: inherit !important;
}
p {
color: #999;
margin-bottom: 15px;
font-size: 1rem;
font-weight: 300;
line-height: 1.3rem;
}
span {
max-width: 30%;
font-size: .8rem;
font-weight: 300;
color: #bbb;
vertical-align: middle;
}
ul {
float: right;
max-width: 50%;
vertical-align: top;
}
ul li {
font-weight: 300;
font-size: .8rem !important;
padding-top: .2rem;
padding-bottom: .2rem;
}
}
@@ -0,0 +1,49 @@
import { Component, OnDestroy, Input } from '@angular/core';
import { DecimalPipe } from '@angular/common';
import { Router, RouterLink } from '@angular/router';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatDividerModule } from '@angular/material/divider';
import { Observable, Subscription } from 'rxjs';
import { JumpMetaComponent } from './jump-meta.component';
import { Jump } from 'src/app/core/models';
import { JumpsService } from 'src/app/core/services';
@Component({
selector: 'app-jump-preview',
standalone: true,
imports: [
DecimalPipe, RouterLink,
MatDividerModule, MatButtonModule, MatCardModule,
JumpMetaComponent
],
styleUrl: './jump-preview.component.scss',
templateUrl: './jump-preview.component.html'
})
export class JumpPreviewComponent implements OnDestroy {
@Input() jump!: Jump;
@Input() isUser!: boolean;
@Input() showMeta!: boolean;
private _jump: Subscription = new Subscription();
isDeleting = false;
constructor(
private router: Router,
private jumpsService: JumpsService
) { }
ngOnDestroy() {
this._jump.unsubscribe();
}
deleteJump() {
this.isDeleting = true;
const jump$: Observable<boolean> = this.jumpsService.destroy(this.jump.slug);
this._jump = jump$.subscribe({
next: () => {
this.router.navigateByUrl('/');
}
});
}
}
@@ -0,0 +1,314 @@
<div class="row mx-0">
<div class="col-md-6 col-xs-12 px-0">
<form class="mt-3" [formGroup]="searchForm" (ngSubmit)="submitForm()">
<mat-form-field appearance="fill">
<input matInput type="number" placeholder="Rechercher un saut" formControlName="numero">
@if (this.searchForm.controls['numero'].status === 'INVALID') {
<mat-error>{{getErrorMessage()}}</mat-error>
}
</mat-form-field>
<button mat-raised-button color="primary" type="submit" class="ms-2">
<mat-icon aria-label="Rechercher" fontIcon="search"></mat-icon> Rechercher
</button>
<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]="!loading" class="ms-2 fst-italic">Chargement des sauts en cours</span>
</form>
</div>
<div class="col-md-6 col-xs-12 px-0">
<mat-paginator [pageSize]="25" [pageSizeOptions]="[10, 25, 50, 100]" showFirstLastButtons class="mb-1 mt-2">
</mat-paginator>
</div>
</div>
<mat-progress-bar [mode]="loading ? 'query' : 'determinate'" [value]="loading ? 0 : 100" color="navy-light">
</mat-progress-bar>
<table mat-table [dataSource]="jumps" matSort
class="table table-dark table-striped table-hover table-condensed mat-elevation-z2">
<caption [hidden]="jumpsCount || loading">Aucun saut à afficher pour le moment.</caption>
<!-- Numero Column -->
<ng-container matColumnDef="numero">
<th mat-header-cell *matHeaderCellDef mat-sort-header> # </th>
<td class="pl-3 pr-3" mat-cell *matCellDef="let element">
@if (element.numero) {
<!--<a [routerLink]="['/jump', element.slug]" class="accent">{{element.numero}}</a>-->
<button mat-button (click)="openViewDialog(element)" color="primary">{{element.numero}}</button>
}
@if (!element.numero) {
<ngx-skeleton-loader [theme]="{width: '30px', height: '13px', marginBottom: '0px'}">
</ngx-skeleton-loader>
}
</td>
</ng-container>
<!-- Date Column -->
<ng-container matColumnDef="date">
<th class="pl-3" mat-header-cell *matHeaderCellDef> Date </th>
<td class="pl-3 text-nowrap" mat-cell *matCellDef="let element">
@if (element.date) {
{{element.date | date: 'mediumDate'}}
}
@if (!element.numero) {
<ngx-skeleton-loader [theme]="{width: '30px', height: '13px', marginBottom: '0px'}">
</ngx-skeleton-loader>
}
</td>
</ng-container>
<!-- Lieu Column -->
<ng-container matColumnDef="lieu">
<th class="pl-3" mat-header-cell *matHeaderCellDef> Lieu </th>
<td class="pl-3 text-nowrap" mat-cell *matCellDef="let element">
@if (element.lieu) {
{{element.lieu}} - {{element.oaci}}
}
@if (!element.numero) {
<ngx-skeleton-loader [theme]="{width: '30px', height: '13px', marginBottom: '0px'}">
</ngx-skeleton-loader>
}
</td>
</ng-container>
<!-- Oaci Column
<ng-container matColumnDef="oaci">
<th class="pl-3" mat-header-cell *matHeaderCellDef> Oaci </th>
<td class="pl-3 text-nowrap" mat-cell *matCellDef="let element">
<ng-container *ngIf="element.oaci">{{element.oaci}}</ng-container>
<ng-container *ngIf="!element.numero">
<ngx-skeleton-loader [theme]="{width: '30px', height: '13px', marginBottom: '0px'}">
</ngx-skeleton-loader>
</ng-container>
</td>
</ng-container> -->
<!-- Aeronef Column -->
<ng-container matColumnDef="aeronef">
<th class="pl-3" mat-header-cell *matHeaderCellDef> Aeronef </th>
<td class="pl-3 text-nowrap" mat-cell *matCellDef="let element">
@if (element.aeronef) {
{{element.aeronef}} - {{element.imat}}
}
@if (!element.numero) {
<ngx-skeleton-loader [theme]="{width: '30px', height: '13px', marginBottom: '0px'}">
</ngx-skeleton-loader>
}
</td>
</ng-container>
<!-- Imat Column
<ng-container matColumnDef="imat">
<th class="pl-3" mat-header-cell *matHeaderCellDef> Imat </th>
<td class="pl-3 text-nowrap" mat-cell *matCellDef="let element">
<ng-container *ngIf="element.imat">{{element.imat}}</ng-container>
<ng-container *ngIf="!element.numero">
<ngx-skeleton-loader [theme]="{width: '30px', height: '13px', marginBottom: '0px'}">
</ngx-skeleton-loader>
</ng-container>
</td>
</ng-container> -->
<!-- Hauteur Column -->
<ng-container matColumnDef="hauteur">
<th class="pl-3" mat-header-cell *matHeaderCellDef> Hauteur </th>
<td class="pl-3" mat-cell *matCellDef="let element">
@if (element.hauteur) {
{{element.hauteur}}
}
@if (!element.numero) {
<ngx-skeleton-loader [theme]="{width: '30px', height: '13px', marginBottom: '0px'}">
</ngx-skeleton-loader>
}
</td>
</ng-container>
<!-- Voile Column -->
<ng-container matColumnDef="voile">
<th class="pl-3" mat-header-cell *matHeaderCellDef> Voile </th>
<td class="pl-3 text-nowrap" mat-cell *matCellDef="let element">
@if (element.voile) {
{{element.voile}} - {{element.taille}}
}
@if (!element.numero) {
<ngx-skeleton-loader [theme]="{width: '30px', height: '13px', marginBottom: '0px'}">
</ngx-skeleton-loader>
}
</td>
</ng-container>
<!-- Taille Column
<ng-container matColumnDef="taille">
<th class="pl-3" mat-header-cell *matHeaderCellDef> Taille </th>
<td class="text-nowrap text-end pl-3 pr-3" mat-cell *matCellDef="let element">
<ng-container *ngIf="element.taille">{{element.taille}}</ng-container>
<ng-container *ngIf="!element.numero">
<ngx-skeleton-loader [theme]="{width: '30px', height: '13px', marginBottom: '0px'}">
</ngx-skeleton-loader>
</ng-container>
</td>
</ng-container> -->
<!-- Categorie Column -->
<ng-container matColumnDef="categorie">
<th class="pl-3" mat-header-cell *matHeaderCellDef> Categorie </th>
<td class="pl-3 text-nowrap" mat-cell *matCellDef="let element">
@if (element.categorie) {
{{element.categorie}}{{element.categorie && element.module ? ' - ' : ''}}{{element.module}}
}
@if (!element.numero) {
<ngx-skeleton-loader [theme]="{width: '30px', height: '13px', marginBottom: '0px'}">
</ngx-skeleton-loader>
}
</td>
</ng-container>
<!-- Module Column
<ng-container matColumnDef="module">
<th class="pl-3" mat-header-cell *matHeaderCellDef> Module </th>
<td class="pl-3 text-nowrap" mat-cell *matCellDef="let element">
<ng-container *ngIf="element.module">{{element.module}}</ng-container>
<ng-container *ngIf="!element.numero">
<ngx-skeleton-loader [theme]="{width: '30px', height: '13px', marginBottom: '0px'}">
</ngx-skeleton-loader>
</ng-container>
</td>
</ng-container> -->
<!-- Zone Column -->
<ng-container matColumnDef="zone">
<th class="pl-3" mat-header-cell *matHeaderCellDef> Zone </th>
<td class="pl-3 text-nowrap" mat-cell *matCellDef="let element">
@if (element.zone) {
{{element.zone}}
}
@if (!element.numero) {
<ngx-skeleton-loader [theme]="{width: '30px', height: '13px', marginBottom: '0px'}">
</ngx-skeleton-loader>
}
</td>
</ng-container>
<!-- Participants Column -->
<ng-container matColumnDef="participants">
<th class="pl-3" mat-header-cell *matHeaderCellDef> Participants </th>
<td class="pl-3 pr-3" mat-cell *matCellDef="let element">
@if (element.participants) {
{{element.participants}}
}
@if (!element.numero) {
<ngx-skeleton-loader [theme]="{width: '30px', height: '13px', marginBottom: '0px'}">
</ngx-skeleton-loader>
}
</td>
</ng-container>
<!-- Programme Column -->
<ng-container matColumnDef="programme">
<th class="pl-3" mat-header-cell *matHeaderCellDef> Programme </th>
<td class="pl-3" mat-cell *matCellDef="let element">
@if (element.programme) {
{{element.programme}}
}
@if (!element.numero) {
<ngx-skeleton-loader [theme]="{width: '30px', height: '13px', marginBottom: '0px'}">
</ngx-skeleton-loader>
}
</td>
</ng-container>
<!-- Accessoires Column -->
<ng-container matColumnDef="accessoires">
<th class="pl-3" mat-header-cell *matHeaderCellDef> Accessoires </th>
<td class="pl-3 text-nowrap" mat-cell *matCellDef="let element">
@if (element.accessoires) {
{{element.accessoires}}
}
@if (!element.numero) {
<ngx-skeleton-loader [theme]="{width: '30px', height: '13px', marginBottom: '0px'}">
</ngx-skeleton-loader>
}
</td>
</ng-container>
<!-- Dossier Column
<ng-container matColumnDef="dossier">
<th class="pl-3" mat-header-cell *matHeaderCellDef> Dossier </th>
<td class="pl-3 text-nowrap" mat-cell *matCellDef="let element">
<ng-container *ngIf="element.dossier">{{element.dossier}}</ng-container>
<ng-container *ngIf="!element.numero">
<ngx-skeleton-loader [theme]="{width: '30px', height: '13px', marginBottom: '0px'}">
</ngx-skeleton-loader>
</ng-container>
</td>
</ng-container> -->
<!-- Video Column
<ng-container matColumnDef="video">
<th class="pl-3" mat-header-cell *matHeaderCellDef> Video </th>
<td class="pl-3 text-nowrap" mat-cell *matCellDef="let element">
@if (element.video) {
{{element.video}}
}
@if (!element.numero) {
<ngx-skeleton-loader [theme]="{width: '30px', height: '13px', marginBottom: '0px'}">
</ngx-skeleton-loader>
}
</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>
<!-- Actions Column -->
<ng-container matColumnDef="actions">
<th class="pl-3 text-center" mat-header-cell *matHeaderCellDef> {{ showAdd ? 'Actions' : 'Favoris' }} </th>
<td class="pl-2 text-nowrap text-center" mat-cell *matCellDef="let element">
@if (element.numero) {
<button [hidden]="!showAdd" mat-icon-button [matMenuTriggerFor]="actionMenu"
aria-label="Actions sur le saut">
<mat-icon>more_vert</mat-icon>
</button>
<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)">
<mat-icon aria-label="Modifier" fontIcon="edit" color="primary" class="mini"></mat-icon>
<span class="text-primary">Modifier</span>
</button>
<button mat-menu-item (click)="openDeleteDialog(element)">
<mat-icon aria-label="Supprimer" fontIcon="delete" color="raspberry" class="mini"></mat-icon>
<span class="text-raspberry">Supprimer</span>
</button>
</mat-menu>
}
@if (!element.numero) {
<ngx-skeleton-loader [hidden]="!showAdd" appearance="circle"
[theme]="{width: '32px', height: '32px', marginBottom: '0px', marginRight: '7px'}">
</ngx-skeleton-loader>
<ngx-skeleton-loader [hidden]="!showAdd" appearance="circle"
[theme]="{width: '32px', height: '32px', marginBottom: '0px', marginRight: '7px'}">
</ngx-skeleton-loader>
<ngx-skeleton-loader appearance="circle"
[theme]="{width: '20px', height: '20px', marginBottom: '0px', marginRight: '7px'}">
</ngx-skeleton-loader>
}
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns; sticky: true" class="accent"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;" class="text-middle accent"></tr>
</table>
@@ -0,0 +1,29 @@
/* TODO(mdc-migration): The following rule targets internal classes of card that may no longer apply for the MDC version. */
mat-card-content {
padding-bottom: 10px;
}
.main-page {
.feed-toggle {
margin-bottom: -1px;
}
.sidebar {
padding: 5px 10px 10px;
background: #f3f3f3;
border-radius: 4px;
p {
margin-bottom: .2rem;
}
}
}
.table.table-condensed.mat-mdc-table {
tr {
height: inherit;
}
td {
vertical-align: middle;
}
}
.mat-mdc-progress-spinner {
display: inline-block;
}
@@ -0,0 +1,472 @@
import { AfterContentChecked, Component, EventEmitter, Input, Output, OnChanges, OnDestroy, ViewChild } from '@angular/core';
import { CommonModule, DecimalPipe, DatePipe } from '@angular/common';
import { UntypedFormBuilder, UntypedFormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms';
import { Router, RouterLink } from '@angular/router';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatDialog, MatDialogModule } from '@angular/material/dialog';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { MatMenuModule } from '@angular/material/menu';
import { MatNativeDateModule } from '@angular/material/core';
import { MatPaginator, MatPaginatorIntl, MatPaginatorModule } from '@angular/material/paginator';
import { MatProgressBarModule } from '@angular/material/progress-bar';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatSort, MatSortModule } from '@angular/material/sort';
import { MatTableDataSource, MatTableModule } from '@angular/material/table';
import { MatSnackBar, MatSnackBarHorizontalPosition, MatSnackBarModule, MatSnackBarVerticalPosition } from '@angular/material/snack-bar';
import { Observable, Subscription } from 'rxjs';
import { take } from 'rxjs/operators';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { FrenchPaginator } from 'src/app/components/shared/french-paginator.component';
import { ColumnDefinition, Errors, Jump, JumpByDay, JumpFile, JumpFileType, JumpList, JumpListConfig, jumpColumns } from 'src/app/core/models';
import { JumpsService, JumpTableService } from 'src/app/core/services';
import { JumpAddDialogComponent, JumpDeleteDialogComponent, JumpEditDialogComponent, JumpViewDialogComponent } from 'src/app/components/logbook/dialogs'
@Component({
standalone: true,
imports: [
CommonModule, DecimalPipe, DatePipe, RouterLink, FormsModule, ReactiveFormsModule, NgxSkeletonLoaderModule,
MatButtonModule, MatCardModule, MatDialogModule,
MatFormFieldModule, MatIconModule, MatInputModule, MatNativeDateModule, MatMenuModule,
MatPaginatorModule, MatProgressBarModule, MatProgressSpinnerModule, MatSnackBarModule, MatSortModule, MatTableModule,
JumpAddDialogComponent, JumpDeleteDialogComponent, JumpEditDialogComponent, JumpViewDialogComponent
],
selector: 'app-jump-table',
styleUrl: './jump-table.component.scss',
templateUrl: './jump-table.component.html',
providers: [{provide: MatPaginatorIntl, useClass: FrenchPaginator}]
})
export class JumpTableComponent implements OnChanges, OnDestroy, AfterContentChecked {
private _jumps: Subscription = new Subscription();
private _subscriptions: Array<Subscription> = [];
//private _jump: Subscription = new Subscription();
private _query: JumpListConfig = { type: 'all', filters: {} };
//private _lastjump: Jump = {} as Jump;
private _currentPage = 1;
public errors!: Errors;
public jump: Jump = {} as Jump;
public jumps!: MatTableDataSource<Jump>;
public jumpsCount = 0;
/* public displayedColumns: string[] = [
'numero', 'date', 'lieu', 'oaci', 'aeronef',
'imat', 'hauteur', 'voile', 'taille', 'categorie',
'module', 'participants', 'programme', 'accessoires',
'zone', 'files', 'actions'
];*/
public displayedColumns: string[] = [
'numero', 'date', 'lieu', 'aeronef',
'hauteur', 'voile', 'categorie',
'participants', 'zone', 'accessoires',
'files', 'actions'
];
//public displayedColumns: string[] = jumpColumns.map((col) => col.key)
public columnsSchema: Array<ColumnDefinition> = jumpColumns;
public searchForm: UntypedFormGroup;
public loading = false;
public isSubmitting = false;
public isDeleting = false;
public tableRefresh = false;
public jumpRefresh = false;
//public valid: any = {};
@ViewChild(MatSort) sort!: MatSort;
@ViewChild(MatPaginator) paginator!: MatPaginator;
constructor(
private router: Router,
private _jumpsService: JumpsService,
private _jumpTableService: JumpTableService,
private dialog: MatDialog,
private fb: UntypedFormBuilder,
private snackBar: MatSnackBar
) {
this._resetErrors();
this.searchForm = this.fb.group({
//numero: ['', Validators.required]
numero: ''
});
}
@Input() title = '';
@Input() limit = 0;
@Input() showAdd = false;
@Input() isUser = false;
@Input()
set config(config: JumpListConfig) {
if (config) {
this._query = config;
this._currentPage = 1;
}
}
get config(): JumpListConfig {
return this._query;
}
@Output() countChange: EventEmitter<number> = new EventEmitter();
ngOnChanges() {
this.loading = true;
this._query = this.config;
this.runQuery();
//console.log('ngOnChanges event in jump-table component');
}
ngOnDestroy() {
this._jumps.unsubscribe();
this._subscriptions.forEach(subscription => {
subscription.unsubscribe();
});
}
ngAfterContentChecked() {
if (this.jumpRefresh != this._jumpTableService.jumpRefresh) {
this.jumpRefresh = this._jumpTableService.jumpRefresh;
this.runQuery();
//console.log('table has been refreshed in jump-table component');
}
}
runQuery() {
/*
const jump$: Observable<Jump> = this._jumpsService.getLastJump();
this._jump = jump$.subscribe((jump) => {
this._lastjump = jump;
});
*/
if (this.limit) {
this._query.filters.limit = this.limit;
this._query.filters.offset = (this.limit * (this._currentPage - 1));
} else {
this._query.filters.limit = 0;
this._query.filters.offset = 0;
}
/*
this._query.filters.numeroRangeStart = 1;
this._query.filters.numeroRangeEnd = 0;
this._query.filters.tailleRangeStart = 135;
this._query.filters.tailleRangeEnd = 230;
this._query.filters.participantsRangeStart = 1;
this._query.filters.participantsRangeEnd = 10;
this._query.filters.hauteurRangeStart = 0;
this._query.filters.hauteurRangeEnd = 8000;
*/
const jumps$: Observable<JumpList> = this._jumpsService.query(this._query);
this._jumps = jumps$.subscribe({
next: (data: JumpList) => {
//this.updateJumps(data.jumps);
//this.linkSkydiverIdJumps();
//this.getKmlJumpFiles(data.jumps);
this.loading = false;
this.jumps = new MatTableDataSource<Jump>(data.jumps);
this.jumpsCount = data.jumpsCount;
this.countChange.emit(this.jumpsCount);
this.paginator.length = data.jumpsCount;
this.jumps.paginator = this.paginator;
this.jumps.sort = this.sort;
this._resetErrors();
},
error: (err) => {
this.errors = err;
}
});
}
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) {
this.isDeleting = true;
this._subscriptions.push(
this._jumpsService.destroy(slug)
.pipe(take(1))
.subscribe(() => {
this.router.navigateByUrl('/');
})
);
}
openDeleteDialog(jump: Jump): void {
const dialogRef = this.dialog.open(JumpDeleteDialogComponent, {
width: '60vw',
data: jump
});
this._subscriptions.push(
dialogRef.afterClosed()
.pipe(take(1))
.subscribe(result => {
if (result != undefined) {
this._onDelete(result.slug);
}
})
);
}
openEditDialog(jump: Jump): void {
const dialogRef = this.dialog.open(JumpEditDialogComponent, {
width: '70vw',
data: jump
});
this._subscriptions.push(
dialogRef.afterClosed()
.pipe(take(1))
.subscribe(result => {
if (result != undefined) {
this._onEdit(result);
}
})
);
}
openViewDialog(jump: Jump): void {
const dialogRef = this.dialog.open(JumpViewDialogComponent, {
width: '60vw',
data: jump
});
dialogRef.afterClosed();
}
getErrorMessage() {
if (this.searchForm.controls['numero'].errors !== null) {
return 'Vous devez indiquer une valeur';
}
return '';
}
submitForm() {
if (this.searchForm.valid) {
this._query.filters.numero = this.searchForm.value.numero;
this._resetErrors();
this.runQuery();
}
}
private _onDelete(slug: string): void {
try {
this.isDeleting = true;
this._subscriptions.push(
this._jumpsService.destroy(slug)
.pipe(take(1))
.subscribe({
next: () => {
this._resetErrors();
this._openSnackBar(`Le saut a été supprimé !`, 'OK');
this.runQuery();
this.tableRefresh = !this.tableRefresh;
this._jumpTableService.updateTableRefresh(this.tableRefresh);
},
error: (err) => {
this.errors = err;
}
})
);
} catch (error) {
console.error(error);
}
}
private _onEdit(jump: Jump): void {
try {
this.isSubmitting = true;
this._subscriptions.push(
this._jumpsService.update(jump)
.pipe(take(1))
.subscribe({
next: (jump) => {
this._resetErrors();
this._openSnackBar(`Le saut n°${jump.numero} a été mis à jour !`, 'OK');
this.runQuery();
this.tableRefresh = !this.tableRefresh;
this._jumpTableService.updateTableRefresh(this.tableRefresh);
},
error: (err) => {
this.errors = err;
this.isSubmitting = false;
}
})
);
} catch (error) {
console.error(error);
}
}
private _openSnackBar(content: string, title: string) {
this.snackBar.open(content, title, {
horizontalPosition: <MatSnackBarHorizontalPosition>'end',
verticalPosition: <MatSnackBarVerticalPosition>'bottom',
duration: 4000,
});
}
private _resetErrors(): void {
this.errors = { errors: {} };
}
/*
disableSubmit(id: number) {
if (this.valid[id]) {
return Object.values(this.valid[id]).some((item) => item === false)
}
return false
}
inputHandler(e: any, id: number, key: string) {
if (!this.valid[id]) {
this.valid[id] = {}
}
this.valid[id][key] = e.target.validity.valid
}
*/
/*
isAllSelected() {
return this.jumps.data.every((item) => item.isSelected)
}
isAnySelected() {
return this.jumps.data.some((item) => item.isSelected)
}
selectAll(event: any) {
this.jumps.data = this.jumps.data.map((item) => ({
...item,
isSelected: event.checked,
}))
}
*/
}
+14
View File
@@ -0,0 +1,14 @@
export * from './accordion';
export * from './dashboard-components';
export * from './helpers-chart';
export * from './helpers-jump';
//export * from './layout';
export * from './layout/footer.component';
export * from './layout/full.component';
export * from './layout/header.component';
export * from './list-errors/list-errors.component';
export * from './spinner/spinner.component';
export * from './confirmed.validator';
export * from './french-paginator.component';
export * from './menu-items';
export * from './show-authed.directive';
@@ -0,0 +1,8 @@
<footer class="footer">
<div class="container-fluid text-end">
<a class="me-1" [href]="siteLink" target="_blank">{{ author }}</a>
<span class="copyright">
&copy; {{ today | date: 'yyyy' }}
</span>
</div>
</footer>
@@ -0,0 +1,24 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { FooterComponent } from './footer.component';
describe('FooterComponent', () => {
let component: FooterComponent;
let fixture: ComponentFixture<FooterComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [FooterComponent]
})
.compileComponents();
fixture = TestBed.createComponent(FooterComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,14 @@
import { Component } from '@angular/core';
import { DatePipe } from '@angular/common';
@Component({
selector: 'app-layout-footer',
templateUrl: './footer.component.html',
standalone: true,
imports: [ DatePipe ]
})
export class FooterComponent {
today: number = Date.now();
siteLink = 'https://www.rampeur.com';
author = 'Solide Apps';
}
+288
View File
@@ -0,0 +1,288 @@
<mat-toolbar class="topbar bg-primary">
<div class="navbar-header">
<a class="navbar-brand" href="/">
<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">
<path id="Jumper_2" fill="#FFFFFF"
d="M171.086,25.274c-0.071-0.029-0.119-0.065-0.17-0.069c-0.527-0.06-1.051-0.143-1.584-0.136
c-0.198,0.003-0.39,0.033-0.574,0.105c-0.296,0.114-0.593,0.23-0.895,0.333c-0.345,0.115-0.697,0.207-1.065,0.201
c-0.258-0.002-0.495-0.071-0.721-0.2c-0.356-0.205-0.53-0.527-0.548-0.945c-0.012-0.196,0.029-0.382,0.089-0.567
c0.058-0.193,0.176-0.345,0.338-0.445c0.184-0.112,0.372-0.218,0.574-0.294c0.385-0.147,0.777-0.265,1.15-0.446
c0.148-0.074,0.32-0.103,0.479-0.151c0.218-0.062,0.419-0.158,0.595-0.312c0.434-0.371,0.938-0.584,1.491-0.661
c0.028-0.004,0.06-0.009,0.083-0.015c0.01-0.002,0.019-0.011,0.05-0.028c0.087-0.373,0.117-0.77-0.016-1.147
c-0.177-0.5-0.298-1.012-0.398-1.533c-0.112-0.575-0.242-1.149-0.367-1.723c-0.063-0.291-0.118-0.583-0.108-0.887
c0.002-0.045-0.016-0.091-0.021-0.13c-0.107-0.045-0.194-0.007-0.285,0.013c-0.372,0.086-0.746,0.167-1.133,0.151
c-0.073-0.004-0.149-0.009-0.224-0.013c-0.27-0.013-0.538-0.031-0.808-0.037c-0.318-0.011-0.633-0.011-0.95-0.011
c-0.289,0.004-0.519-0.09-0.699-0.341c-0.367-0.511-0.751-1.009-1.126-1.512c-0.229-0.306-0.453-0.613-0.685-0.915
c-0.108-0.14-0.197-0.284-0.129-0.482c-0.437-0.519-0.708-1.134-0.993-1.742c-0.088-0.19-0.211-0.363-0.317-0.544
c-0.107-0.18-0.133-0.377-0.111-0.584c0.014-0.124,0.057-0.223,0.204-0.274c0.029,0.046,0.058,0.106,0.103,0.145
c0.038,0.034,0.097,0.045,0.162,0.071c0.021,0.269,0.152,0.489,0.309,0.694c0.182,0.236,0.397,0.287,0.703,0.17
c0.092-0.236,0.032-0.542,0.258-0.733c0.112,0.033,0.114,0.126,0.119,0.21c0.01,0.23,0.02,0.463,0.02,0.693
c0,0.154-0.018,0.307-0.026,0.461c-0.012,0.173-0.023,0.345-0.027,0.518c-0.006,0.17,0.06,0.315,0.16,0.45
c0.111-0.039,0.216-0.076,0.32-0.113c0.063,0.058,0.113,0.097,0.155,0.146c0.266,0.324,0.521,0.654,0.846,0.919
c0.131,0.107,0.228,0.254,0.343,0.381c0.112,0.125,0.231,0.25,0.35,0.374c0.101,0.1,0.216,0.133,0.361,0.097
c0.279-0.068,0.563-0.121,0.848-0.177c0.101-0.021,0.203-0.025,0.302-0.044c0.212-0.037,0.425-0.065,0.629-0.124
c0.243-0.07,0.488-0.12,0.743-0.13c0.354-0.015,0.704-0.054,1.051-0.123c0.127-0.025,0.26-0.023,0.389-0.037
c0.081-0.009,0.159-0.031,0.266-0.052c-0.056-0.1-0.097-0.17-0.143-0.25c-0.106,0-0.212-0.01-0.312,0.003
c-0.449,0.049-0.868-0.042-1.291-0.289c-0.02-0.063-0.054-0.152-0.068-0.245c-0.038-0.238-0.059-0.478-0.099-0.715
c-0.025-0.154,0.004-0.288,0.079-0.416c0.312-0.536,0.626-1.073,0.94-1.61c0.02-0.031,0.042-0.064,0.066-0.091
c0.554-0.622,1.223-0.968,2.056-0.914c0.14,0.008,0.28,0.021,0.413,0.053c0.713,0.166,1.136,0.625,1.263,1.37
c0.054,0.326,0.083,0.65,0.022,0.979c-0.019,0.094-0.013,0.194-0.021,0.291c-0.021,0.314-0.129,0.58-0.433,0.717
c-0.063,0.029-0.121,0.083-0.188,0.13c0.103,0.156,0.191,0.303,0.292,0.437c0.036,0.049,0.104,0.081,0.161,0.098
c0.072,0.02,0.149,0.017,0.223,0.014c0.188-0.009,0.372,0.009,0.555,0.047c0.116,0.024,0.242,0.022,0.36,0.014
c0.43-0.035,0.854-0.038,1.282,0.021c0.405,0.054,0.818,0.05,1.227,0.071c0.161,0.008,0.312-0.035,0.454-0.121
c0.235-0.144,0.482-0.237,0.752-0.284c0.2-0.034,0.397-0.109,0.587-0.188c0.666-0.272,1.333-0.545,1.993-0.833
c0.385-0.167,0.775-0.258,1.194-0.245c0.55,0.018,0.481,0.081,1.14-0.292c0.271-0.153,0.546-0.288,0.845-0.372
c0.043-0.014,0.088-0.02,0.146-0.031c0.045,0.076,0.087,0.147,0.131,0.224c0.047-0.018,0.095-0.028,0.131-0.051
c0.356-0.21,0.713-0.417,1.063-0.637c0.254-0.16,0.429-0.393,0.531-0.688c0.051-0.146,0.119-0.284,0.189-0.42
c0.052-0.102,0.107-0.206,0.186-0.288c0.25-0.259,0.501-0.517,0.766-0.76c0.16-0.146,0.344-0.264,0.515-0.397
c0.065-0.049,0.125-0.086,0.208-0.043c0.059,0.102,0.116,0.204,0.191,0.332c-0.04,0.05-0.081,0.113-0.131,0.162
c-0.147,0.136-0.298,0.264-0.445,0.397c-0.176,0.157-0.248,0.37-0.312,0.626c0.245-0.074,0.396-0.362,0.67-0.234
c0.079,0.141,0.054,0.271-0.03,0.384c-0.074,0.097-0.161,0.186-0.252,0.268c-0.119,0.11-0.254,0.198-0.364,0.315
c-0.137,0.143-0.275,0.291-0.376,0.462c-0.249,0.411-0.558,0.749-0.957,0.999c-0.311,0.193-0.631,0.37-0.947,0.556
c-0.016,0.009-0.033,0.015-0.05,0.026c-0.442,0.346-0.979,0.507-1.469,0.757c-0.334,0.172-0.678,0.32-1.015,0.484
c-0.364,0.174-0.716,0.366-1.044,0.608c-0.42,0.312-0.881,0.548-1.395,0.646c-0.385,0.071-0.771,0.126-1.158,0.186
c-0.764,0.119-1.528,0.231-2.291,0.357c-0.151,0.024-0.316,0.034-0.458,0.131c0.01,0.127,0.087,0.213,0.144,0.311
c0.428,0.748,0.842,1.501,1.209,2.285c0.067,0.146,0.137,0.294,0.198,0.445c0.18,0.426,0.337,0.855,0.359,1.328
c0.016,0.328,0.065,0.651,0.101,0.977c0.006,0.05,0.006,0.098,0.01,0.146c0.03,0.483-0.147,0.834-0.577,1.057
c-0.394,0.201-0.784,0.399-1.196,0.542c-0.17,0.005-0.318-0.111-0.515-0.07c0.015,0.144,0.024,0.274,0.042,0.405
c0.063,0.499,0.136,0.994,0.196,1.491c0.047,0.379,0.018,0.752-0.115,1.112c-0.073,0.197-0.152,0.392-0.226,0.591
c-0.066,0.192-0.18,0.335-0.356,0.427c-0.038,0.02-0.07,0.052-0.146,0.107c0.355,0.054,0.672-0.048,0.981,0.086
c0.024-0.072,0.045-0.124,0.057-0.178c0.036-0.158,0.052-0.323,0.101-0.479c0.108-0.354,0.245-0.422,0.59-0.298
c0.356,0.129,0.679,0.331,0.923,0.627c0.222,0.273,0.491,0.473,0.785,0.645c0.187,0.11,0.376,0.211,0.554,0.333
c0.411,0.286,0.662,0.696,0.835,1.167c0.002,0.01,0.002,0.021,0.006,0.027c0.113,0.356-0.139,0.573-0.484,0.499
c-0.069-0.012-0.141-0.053-0.202-0.094c-0.078-0.049-0.154-0.105-0.228-0.166c-0.382-0.329-0.378-0.286-0.842-0.239
c-0.082,0.008-0.162,0.035-0.247,0.047c-0.423,0.082-0.451,0.129-1.049-0.029c-0.246,0.119-0.516,0.244-0.784,0.382
c-0.393,0.201-0.773,0.426-1.178,0.604c-0.601,0.261-1.18,0.568-1.764,0.866c-0.522,0.266-1.067,0.46-1.646,0.53
c-0.506,0.064-1.005,0.049-1.497-0.101c-0.706-0.209-1.172-0.79-1.244-1.546c-0.02-0.206,0.028-0.401,0.088-0.597
c0.13-0.405,0.331-0.774,0.551-1.134c0.443-0.73,0.954-1.403,1.484-2.068C170.955,25.439,171.013,25.367,171.086,25.274z" />
<path id="Head_Up" fill="#FFFFFF" d="M56.496,12.915l-1.858,10.843h-5.11l1.156-6.74h-5.417l-1.155,6.74H39l1.858-10.843H56.496z
M47.125,6.172l-0.904,5.275h-5.112l0.904-5.275H47.125z M57.651,6.172l-0.932,5.275h-5.111l0.933-5.275H57.651z M72.124,12.915
l-0.684,4.103h-9.115l-0.25,1.466h9.11l-0.91,5.275H56.06l1.857-10.843L72.124,12.915L72.124,12.915z M73.29,6.172l-0.911,5.275
h-14.21l0.904-5.275H73.29z M87.222,23.758h-5.118l-0.17-2.345h-4.818l-0.97,2.345h-5.131l4.488-10.843h3.819
c0.836,0.002,1.273,0.003,1.311,0.003l-1.685,4.1h2.702l-0.313-4.103h5.118L87.222,23.758z M86.34,11.448H76.109l2.183-5.275h7.677
L86.34,11.448z M105.426,12.915l-1.145,6.74c-0.19,1.134-0.745,2.1-1.664,2.901c-0.919,0.801-1.928,1.201-3.028,1.201H87.932
l1.858-10.843h5.083l-0.926,5.567h5.395l0.946-5.567H105.426z M105.931,9.573c0,0.234-0.02,0.469-0.058,0.704l-0.199,1.171H90.041
l0.905-5.275h11.657c0.967,0,1.763,0.323,2.388,0.967S105.931,8.596,105.931,9.573z M132.414,12.915l-1.15,6.74
c-0.189,1.134-0.745,2.1-1.663,2.901c-0.92,0.802-1.93,1.202-3.029,1.202h-7.677c-0.965,0-1.763-0.322-2.389-0.968
c-0.624-0.643-0.938-1.455-0.938-2.434c0-0.232,0.02-0.468,0.058-0.702l1.121-6.74h5.141l-0.953,5.567h5.42l0.951-5.567H132.414z
M123.039,6.172l-0.932,5.275h-5.11l0.931-5.275H123.039z M133.565,6.172l-0.931,5.275h-5.107l0.93-5.275H133.565z M149.488,12.915
c-0.192,1.133-0.742,2.099-1.651,2.901c-0.911,0.801-1.919,1.201-3.019,1.201h-6.55l-1.155,6.741h-5.11l1.857-10.843L149.488,12.915
L149.488,12.915z M150,9.573c0,0.234-0.02,0.469-0.058,0.704l-0.198,1.171H134.11l0.905-5.275h11.658
c0.965,0,1.763,0.323,2.389,0.967C149.687,7.784,150,8.596,150,9.573z" />
<path id="Jumper_1" fill="#FFFFFF"
d="M13.163,30.84c0.039-0.191-0.035-0.336-0.106-0.482c-0.076-0.151-0.164-0.299-0.244-0.45
c-0.104-0.196-0.167-0.406-0.177-0.634c-0.014-0.305-0.088-0.598-0.225-0.866c0.163-1.38,0.545-2.71,0.893-4.042
c0.134-0.521,0.35-1.01,0.566-1.514c0.104-0.026,0.209-0.055,0.314-0.08c0.848-0.205,1.694-0.425,2.546-0.612
c0.803-0.178,1.615-0.32,2.42-0.485c0.192-0.036,0.377-0.003,0.562,0.032c0.072,0.014,0.136,0.068,0.222,0.114
c-0.024,0.113-0.035,0.212-0.065,0.305c-0.195,0.626-0.381,1.253-0.59,1.872c-0.226,0.671-0.352,1.365-0.533,2.048
c-0.105,0.399-0.209,0.8-0.309,1.199c-0.055,0.226-0.105,0.444-0.027,0.684c0.039,0.12-0.016,0.278-0.039,0.419
c-0.026,0.163-0.031,0.339-0.17,0.47c-0.031,0.029-0.052,0.089-0.056,0.136c-0.042,0.44-0.08,0.883-0.118,1.324
c-0.001,0.021,0.009,0.039,0.019,0.073c0.093,0.102,0.227,0.127,0.368,0.138c0.34,0.021,0.674-0.011,1.005-0.084
c0.298-0.062,0.588-0.06,0.878,0.043c0.77,0.277,1.526,0.162,2.273-0.104c0.093-0.032,0.181-0.085,0.262-0.14
c0.093-0.062,0.174-0.139,0.167-0.273c-0.048-0.142-0.18-0.174-0.304-0.2c-0.214-0.047-0.429-0.087-0.644-0.118
c-0.16-0.024-0.306-0.079-0.443-0.164c-0.109-0.066-0.227-0.127-0.339-0.188c-0.276-0.147-0.526-0.323-0.704-0.594
c-0.06-0.091-0.144-0.165-0.213-0.244c0.1-0.127,0.21-0.242,0.29-0.375c0.077-0.128,0.122-0.275,0.176-0.416
c0.349-0.88,0.658-1.773,0.846-2.706c0.055-0.265,0.071-0.539,0.124-0.807c0.112-0.573,0.232-1.147,0.355-1.718
c0.096-0.455,0.162-0.915,0.189-1.377c0.018-0.293-0.018-0.585-0.099-0.871c-0.099-0.349-0.312-0.603-0.625-0.772
c-0.24-0.133-0.497-0.208-0.762-0.25c-0.471-0.077-0.947-0.097-1.425-0.055c-0.623,0.058-1.243,0.138-1.863,0.196
c-0.559,0.055-1.111-0.003-1.656-0.127c-0.443-0.1-0.719-0.487-0.693-0.955c0.011-0.18,0.032-0.363,0.062-0.542
c0.114-0.746,0.232-1.495,0.358-2.238c0.031-0.178,0.093-0.352,0.156-0.521c0.187-0.518,0.299-1.045,0.271-1.6
c-0.031-0.608,0.041-1.209,0.139-1.807c0.044-0.269,0.072-0.541,0.085-0.812c0.018-0.309,0.115-0.572,0.341-0.78
c0.302-0.278,0.608-0.55,0.909-0.83c0.108-0.101,0.215-0.208,0.302-0.33c0.112-0.153,0.272-0.238,0.422-0.329
c0.229-0.14,0.401-0.321,0.536-0.554c0.198-0.339,0.406-0.669,0.608-1.006c0.072-0.122,0.132-0.249,0.19-0.377
c0.041-0.086,0.049-0.177-0.062-0.249c-0.299,0.062-0.337,0.428-0.604,0.565c-0.023-0.056-0.062-0.104-0.062-0.153
c-0.006-0.323,0.03-0.641,0.234-0.914c0.045-0.063,0.104-0.124,0.073-0.228c-0.087-0.012-0.18-0.021-0.269-0.039
c-0.024-0.005-0.043-0.038-0.077-0.068c-0.04,0.025-0.093,0.041-0.119,0.076c-0.188,0.233-0.373,0.47-0.558,0.708
c-0.022,0.032-0.042,0.07-0.052,0.108c-0.1,0.401-0.227,0.794-0.249,1.213c-0.006,0.119-0.04,0.237-0.073,0.352
c-0.012,0.043-0.056,0.078-0.095,0.129c-0.219-0.2-0.468-0.151-0.71-0.095c-0.182,0.041-0.36,0.104-0.558,0.163
c-0.012-0.087-0.022-0.166-0.03-0.243c-0.036-0.422-0.053-0.843-0.154-1.26c-0.104-0.431-0.347-0.74-0.724-0.949
c-0.273-0.153-0.56-0.267-0.872-0.282c-0.69-0.034-1.224,0.274-1.64,0.817c-0.211,0.279-0.305,0.608-0.36,0.95
c-0.065,0.399-0.129,0.798-0.001,1.199c0.058,0.183,0.106,0.368,0.157,0.553c0.089,0.324,0.078,0.361-0.169,0.663
c-0.057-0.024-0.117-0.052-0.18-0.076c-0.147-0.058-0.292-0.131-0.444-0.167c-0.33-0.076-0.645-0.196-0.959-0.322
c-0.405-0.164-0.816-0.313-1.22-0.482c-0.284-0.118-0.563-0.252-0.84-0.387C8.42,7.775,7.884,7.465,7.302,7.254
C7.118,7.188,6.975,7.087,6.912,6.876C6.879,6.765,6.789,6.668,6.71,6.58C6.49,6.332,6.236,6.127,5.95,5.956
c-0.525-0.31-1.005-0.688-1.431-1.13C4.365,4.667,4.197,4.557,3.982,4.53C3.754,4.501,3.627,4.372,3.581,4.148
C3.578,4.14,3.577,4.13,3.575,4.119C3.443,3.435,3.222,2.774,3.044,2.104C3.014,1.998,2.969,1.895,2.914,1.8
C2.758,1.539,2.555,1.316,2.328,1.119c-0.079-0.07-0.162-0.156-0.302-0.102C1.982,1.224,1.982,1.418,2.121,1.608
c0.125,0.172,0.152,0.388,0.2,0.601C2.244,2.258,2.17,2.304,2.083,2.361c0,0.077-0.014,0.156,0.001,0.232
C2.159,2.979,2.241,3.363,2.314,3.75c0.038,0.186,0.074,0.376,0.081,0.567c0.006,0.23,0.087,0.408,0.265,0.547
c0.071,0.055,0.133,0.123,0.203,0.178c0.142,0.117,0.26,0.24,0.289,0.441c0.018,0.13,0.114,0.227,0.21,0.321
C3.917,6.352,4.47,6.902,5.018,7.459C5.145,7.586,5.246,7.74,5.36,7.878c0.174,0.208,0.34,0.421,0.524,0.62
C6.029,8.654,6.209,8.75,6.417,8.812c0.236,0.07,0.468,0.167,0.694,0.269c0.699,0.313,1.394,0.633,2.092,0.951
c0.287,0.131,0.52,0.329,0.721,0.594c-0.027,0.083-0.049,0.168-0.083,0.251c-0.22,0.542-0.412,1.091-0.569,1.658
c-0.109,0.397-0.261,0.783-0.393,1.174c-0.196,0.582-0.388,1.167-0.475,1.779c-0.062,0.441-0.072,0.887,0.024,1.325
c0.023,0.106,0.064,0.213,0.113,0.312c0.061,0.123,0.161,0.212,0.302,0.215c0.178,0.005,0.284,0.099,0.389,0.237
C9.329,17.7,9.46,17.795,9.596,17.92c-0.051,0.095-0.103,0.203-0.16,0.304c-0.286,0.511-0.52,1.046-0.674,1.611
c-0.087,0.322-0.123,0.652-0.104,0.989c0.033,0.619,0.318,1.092,0.825,1.432c0.241,0.16,0.506,0.272,0.787,0.352
c0.084,0.022,0.167,0.051,0.248,0.075c0.07,0.148,0.041,0.292,0.04,0.43c-0.01,0.84-0.021,1.679-0.038,2.516
c-0.006,0.354,0.007,0.706,0.076,1.055c0.056,0.28,0.042,0.56,0.016,0.845c-0.021,0.219-0.023,0.443-0.032,0.663
c-0.002,0.041-0.01,0.089,0.009,0.119c0.116,0.2,0.029,0.395-0.022,0.584c-0.073,0.275-0.164,0.542-0.172,0.829
c-0.003,0.098,0,0.197,0,0.297c0.186,0.125,0.297,0.279,0.346,0.488c0.042,0.184,0.191,0.282,0.359,0.332
c0.16,0.046,0.328,0.078,0.496,0.092c0.365,0.032,0.734,0.06,1.098,0.066C12.854,31.002,13.037,31.017,13.163,30.84z" />
</svg>
</span>
</a>
</div>
<button mat-icon-button (click)="snav.toggle()" value="sidebarclosed">
<mat-icon fontIcon="menu"></mat-icon>
</button>
<span class="flex-spacer"></span>
<ng-container *appShowAuthed="true">
<div class="position-relative">
<button [matMenuTriggerFor]="profile" mat-icon-button>
<img [src]="currentUser.image" alt="user" class="profile-pic">
</button>
<mat-menu #profile="matMenu">
<button mat-menu-item [routerLink]="['/profile', currentUser.username]" routerLinkActive="active">
<mat-icon fontIcon="account_circle" color="primary"></mat-icon> Mon compte
</button>
<button mat-menu-item routerLink="/settings" routerLinkActive="active">
<mat-icon fontIcon="settings" color="primary"></mat-icon> Paramètres
</button>
<button mat-menu-item (click)="goToGitHub()">
<mat-icon fontIcon="code" color="primary"></mat-icon> GitHub
</button>
<button mat-menu-item>
<mat-icon fontIcon="notifications_off" color="primary"></mat-icon> Désactiver les notifications
</button>
<mat-divider class="my-1"></mat-divider>
<button mat-menu-item (click)="logout()">
<mat-icon fontIcon="logout" color="warn"></mat-icon> Se déconnecter
</button>
</mat-menu>
</div>
<span [matMenuTriggerFor]="profile" class="fw-light me-1 text-navy pointer">&#64;{{ currentUser.username }}</span>
</ng-container>
<ng-container *appShowAuthed="false">
<span>
<button mat-button routerLink="/login" routerLinkActive="active">Se connecter</button>
<button mat-button routerLink="/register" routerLinkActive="active">Créer un compte</button>
</span>
</ng-container>
</mat-toolbar>
<mat-sidenav-container class="app-sidenav-container" [style.marginTop.px]="mobileQuery.matches ? 0 : 0">
<mat-sidenav #snav id="snav" class="dark-sidebar pl-xs" [mode]="mobileQuery.matches ? 'side' : 'over'" fixedTopGap="0" [opened]="mobileQuery.matches" [disableClose]="mobileQuery.matches">
<ng-container *appShowAuthed="true">
<div class="user-profile" style="background-image: url('{{ currentUser.bg_image }}');">
<div class="profile-img">
<img [src]="currentUser.image" alt="user">
</div>
<div class="profile-text">
<a [matMenuTriggerFor]="sdprofile" class="d-flex justify-content-end align-items-end">
<span>{{ currentUser.firstname }} {{ currentUser.lastname }}</span>
<mat-icon fontIcon="expand_more" class="ms-3 lh-sm"></mat-icon>
</a>
</div>
<mat-menu #sdprofile="matMenu" class="rounded-0">
<button mat-menu-item [routerLink]="['/profile', currentUser.username]" routerLinkActive="active">
<mat-icon fontIcon="account_box" color="primary"></mat-icon> Mon compte
</button>
<button mat-menu-item routerLink="/settings" routerLinkActive="active">
<mat-icon fontIcon="settings" color="primary"></mat-icon> Paramètres
</button>
<button mat-menu-item>
<mat-icon fontIcon="notifications_off" color="primary"></mat-icon> Désactiver les notifications
</button>
<mat-divider class="my-1"></mat-divider>
<button mat-menu-item (click)="logout()">
<mat-icon fontIcon="exit_to_app" color="warn"></mat-icon> Se déconnecter
</button>
</mat-menu>
</div>
<mat-nav-list appAccordion>
@for (menuitem of menuItems.getMenuItems(); track menuitem) {
<mat-list-item appAccordionLink routerLinkActive="selected" group="{{menuitem.state}}">
@if (menuitem.type === 'link') {
<a appAccordionToggle [routerLink]="['/', menuitem.state]">
<mat-icon [fontIcon]="menuitem.icon" class="mt-0 mr-2"></mat-icon>
<span>{{ menuitem.name }}</span>
<span class="flex-spacer"></span>
@for (badge of menuitem.badge; track badge) {
<span matBadge="{{ badge.value }}" matBadgeColor="warn" matBadgeOverlap="false"></span>
}
</a>
}
@if (menuitem.type === 'menu') {
<a role="button" aria-hidden="true" (click)="toggleShowQCMMenu()" (keyup)="true">
<mat-icon [fontIcon]="menuitem.icon" class="mt-0 mr-2"></mat-icon>
<span>{{ menuitem.name }}</span>
<span class="flex-spacer"></span>
@for (badge of menuitem.badge; track badge) {
<span matBadge="{{ badge.value }}" matBadgeColor="warn" matBadgeOverlap="false"></span>
}
</a>
}
</mat-list-item>
}
@if (showQCMMenu) {
<div>
@for (menuqcm of menuItems.getMenuQCM(); track menuqcm) {
<mat-list-item appAccordionLink routerLinkActive="selected" group="{{ menuqcm.state}}">
@if ( menuqcm.type === 'link') {
<a class="ps-4" appAccordionToggle [routerLink]="['/qcm', menuqcm.state]">
<mat-icon [fontIcon]="menuqcm.icon" class="mt-0 mr-2"></mat-icon>
<span>{{ menuqcm.name }}</span>
<span class="flex-spacer"></span>
</a>
}
</mat-list-item>
}
</div>
}
</mat-nav-list>
</ng-container>
<ng-container *appShowAuthed="false">
<div class="user-profile">
<!-- <div class="profile-img"> <img src="/assets/images/users/user.jpg" alt="user"> </div> -->
</div>
<mat-nav-list appAccordion>
@for (menuitem of menuItems.getMenuAuth(); track menuitem) {
<mat-list-item appAccordionLink routerLinkActive="selected" group="{{menuitem.state}}">
@if (menuitem.type === 'link') {
<a appAccordionToggle [routerLink]="['/', menuitem.state]">
<mat-icon [fontIcon]="menuitem.icon" class="mt-0 mr-2"></mat-icon>
<span>{{ menuitem.name }}</span>
<span class="flex-spacer"></span>
@for (badge of menuitem.badge; track badge) {
<span matBadge="{{ badge.value }}" matBadgeColor="warn" matBadgeOverlap="false"></span>
}
</a>
}
@if (menuitem.type === 'menu') {
<a role="button" aria-hidden="true" (click)="showQCMMenu = !showQCMMenu">
<mat-icon [fontIcon]="menuitem.icon" class="mt-0 mr-2"></mat-icon>
<span>{{ menuitem.name }}</span>
<span class="flex-spacer"></span>
@for (badge of menuitem.badge; track badge) {
<span matBadge="{{ badge.value }}" matBadgeColor="warn" matBadgeOverlap="false"></span>
}
</a>
}
</mat-list-item>
}
@if (showQCMMenu) {
<div>
@for (menuqcm of menuItems.getMenuQCM(); track menuqcm) {
<mat-list-item appAccordionLink routerLinkActive="selected" group="{{ menuqcm.state}}">
@if ( menuqcm.type === 'link') {
<a class="ps-4" appAccordionToggle [routerLink]="['/', menuqcm.state]">
<mat-icon [fontIcon]="menuqcm.icon" class="mt-0 mr-2"></mat-icon>
<span>{{ menuqcm.name }}</span>
<span class="flex-spacer"></span>
</a>
}
</mat-list-item>
}
</div>
}
</mat-nav-list>
</ng-container>
<app-layout-footer></app-layout-footer>
</mat-sidenav>
<mat-sidenav-content class="page-wrapper">
<!--<div class="page-content">-->
<router-outlet><!--<app-spinner></app-spinner>--></router-outlet>
<!--</div>-->
</mat-sidenav-content>
</mat-sidenav-container>
@@ -0,0 +1,24 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { FullComponent } from './full.component';
describe('FullComponent', () => {
let component: FullComponent;
let fixture: ComponentFixture<FullComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [FullComponent]
})
.compileComponents();
fixture = TestBed.createComponent(FullComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
+89
View File
@@ -0,0 +1,89 @@
import { Component, OnInit, OnDestroy, ChangeDetectorRef } from '@angular/core';
import { Router, RouterOutlet, RouterModule } from '@angular/router';
import { MediaMatcher } from '@angular/cdk/layout';
import { MatBadgeModule } from '@angular/material/badge';
import { MatButtonModule } from '@angular/material/button';
import { MatDividerModule } from '@angular/material/divider';
import { MatIconModule } from '@angular/material/icon';
import { MatListModule } from '@angular/material/list';
import { MatSidenavModule } from '@angular/material/sidenav';
import { MatToolbarModule } from '@angular/material/toolbar';
import { MatMenuModule } from '@angular/material/menu';
import { Observable, Subscription } from 'rxjs';
import { User } from 'src/app/core/models';
import { UserService } from 'src/app/core/services';
import { MenuItems, ShowAuthedDirective, AccordionAnchorDirective, AccordionLinkDirective, AccordionDirective } from 'src/app/components/shared';
//import { SpinnerComponent } from 'src/app/components/shared';
//import { HeaderComponent, FooterComponent } from 'src/app/components/shared/layout';
import { FooterComponent } from 'src/app/components/shared/layout';
/** @title Responsive sidenav */
@Component({
selector: 'app-full-layout',
standalone: true,
imports: [
RouterModule, RouterOutlet,
MatBadgeModule, MatButtonModule, MatDividerModule,
MatIconModule, MatListModule, MatMenuModule,
MatSidenavModule, MatToolbarModule,
ShowAuthedDirective, AccordionAnchorDirective,
AccordionLinkDirective, AccordionDirective,
FooterComponent
//HeaderComponent,
//SpinnerComponent
],
providers: [ MenuItems ],
templateUrl: 'full.component.html',
styleUrls: []
})
export class FullComponent implements OnInit, OnDestroy {
private _currentUser: Subscription = new Subscription();
private _mobileQueryListener: () => void;
mobileQuery: MediaQueryList;
currentUser: User = {} as User;
showQCMMenu = false;
today: number = Date.now();
siteLink = 'https://www.rampeur.com';
author = 'Solide Apps';
constructor(
private router: Router,
private userService: UserService,
changeDetectorRef: ChangeDetectorRef,
media: MediaMatcher,
public menuItems: MenuItems
) {
this.mobileQuery = media.matchMedia('(min-width: 768px)');
this._mobileQueryListener = () => changeDetectorRef.detectChanges();
this.mobileQuery.addListener(this._mobileQueryListener);
}
ngOnInit() {
const user$: Observable<User> = this.userService.currentUser;
this._currentUser = user$.subscribe(
(userData) => {
this.currentUser = userData;
}
);
}
ngOnDestroy(): void {
this._currentUser.unsubscribe();
this.mobileQuery.removeListener(this._mobileQueryListener);
}
goToGitHub() {
window.open('https://github.com/rampeur', '_blank');
}
toggleShowQCMMenu() {
this.showQCMMenu = !this.showQCMMenu;
}
logout() {
this.userService.purgeAuth();
this.router.navigateByUrl('/login');
}
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,24 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { HeaderComponent } from './header.component';
describe('HeaderComponent', () => {
let component: HeaderComponent;
let fixture: ComponentFixture<HeaderComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [HeaderComponent]
})
.compileComponents();
fixture = TestBed.createComponent(HeaderComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,65 @@
import { Component, DestroyRef, inject, OnInit } from '@angular/core';
import { Router, RouterLink, RouterLinkActive } from '@angular/router';
import { MatButtonModule } from '@angular/material/button';
import { MatDividerModule } from '@angular/material/divider';
import { MatIconModule } from '@angular/material/icon';
import { MatMenuModule } from '@angular/material/menu';
import { MatToolbarModule } from '@angular/material/toolbar';
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { Observable, Subscription } from 'rxjs';
import { User } from 'src/app/core/models';
import { UserService } from 'src/app/core/services';
@Component({
selector: 'app-layout-header',
templateUrl: './header.component.html',
standalone: true,
imports: [
RouterLink, RouterLinkActive,
MatToolbarModule, MatButtonModule, MatDividerModule,
MatIconModule, MatMenuModule
]
})
export class HeaderComponent implements OnInit {
private _currentUser: Subscription = new Subscription();
private _isAuthenticated: Subscription = new Subscription();
private isAuthenticated = false;
currentUser: User = {} as User;
destroyRef = inject(DestroyRef);
constructor(
private router: Router,
private userService: UserService
) { }
ngOnInit() {
const user$: Observable<User> = this.userService.currentUser.pipe(takeUntilDestroyed(this.destroyRef));
this._currentUser = user$.subscribe(
(userData) => {
this.currentUser = userData;
}
);
const auth$: Observable<boolean> = this.userService.isAuthenticated.pipe(takeUntilDestroyed(this.destroyRef));
this._isAuthenticated = auth$.subscribe(
(value) => {
this.isAuthenticated = value;
}
);
}
goToGitHub() {
window.open('https://github.com/rampeur', '_blank');
}
isLogged(): boolean {
return this.isAuthenticated;
}
logout() {
this.userService.purgeAuth();
this.router.navigateByUrl('/');
}
}
@@ -0,0 +1,3 @@
export * from './footer.component';
export * from './full.component';
export * from './header.component';
@@ -0,0 +1,11 @@
<mat-card appearance="outlined" class="error-messages mb-4" [hidden]="!errorList.length">
<mat-card-content>
@if (errorList.length) {
<ul class="error-messages">
@for (error of errorList; track error) {
<li>{{ error }}</li>
}
</ul>
}
</mat-card-content>
</mat-card>
@@ -0,0 +1,23 @@
import { Component, Input } from '@angular/core';
import { NgForOf, NgIf } from "@angular/common";
import { MatCardModule } from '@angular/material/card';
import { Errors } from 'src/app/core';
@Component({
selector: 'app-list-errors',
templateUrl: './list-errors.component.html',
standalone: true,
imports: [NgIf, NgForOf, MatCardModule]
})
export class ListErrorsComponent {
errorList: string[] = [];
@Input() set errors(errorList: Errors | null) {
this.errorList = errorList
? Object.keys(errorList.errors || {}).map(
(key) => `${key} ${errorList.errors[key]}`,
)
: [];
}
}
+74
View File
@@ -0,0 +1,74 @@
import { Injectable } from '@angular/core';
import { Menu } from 'src/app/core/models/menu.model';
const MENUAUTH: Menu[] = [
{ state: 'login', type: 'link', name: 'Se connecter', icon: 'fingerprint', desc: '' },
{ state: 'register', type: 'link', name: 'Créer un compte', icon: 'person_add_alt', desc: '' },
{ state: 'qcm', type: 'menu', name: 'QCM Brevets', icon: 'rule_folder', desc: '' }
];
const MENUITEMS: Menu[] = [
{ state: 'dashboard', type: 'link', name: 'Dashboard', icon: 'dashboard', desc: '' },
{ state: 'logbook', type: 'link', name: 'Carnet de saut', icon: 'assignment_turned_in', desc: '' },
{ state: 'aeronefs', type: 'link', name: 'Aéronefs', icon: 'flight', desc: '' },
{ state: 'dropzones', type: 'link', name: 'Drop Zones', icon: 'flight_takeoff', desc: '' },
{ state: 'canopies', type: 'link', name: 'Voiles', icon: 'paragliding', desc: '' },
{ state: 'jumps', type: 'link', name: 'Sauts', icon: 'filter_drama', desc: '' },
{ state: 'calculator', type: 'link', name: 'Taille de voile', icon: 'calculate', 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[] = [
{ state: 'bpa', type: 'link', name: 'BPA', icon: 'rule' },
{ state: 'c', type: 'link', name: 'Brevet C', icon: 'rule' },
{ state: 'd', type: 'link', name: 'Brevet D', icon: 'rule' }
];
const MENUPANEL: Menu[] = [
{ state: '#', type: 'link', name: 'Retirer du dashboard', icon: 'add_notes', desc: '' },
{ state: '#', type: 'link', name: 'Paramètres d\'affichage', icon: 'display_settings', desc: '' }
];
const MENUCALCULATOR: Menu[] = [
{ state: '#', type: 'link', name: 'Recherche avancée', icon: 'search', desc: '' },
{ state: '#', type: 'link', name: 'Paramètres d\'affichage', icon: 'display_settings', desc: '' },
{ state: '#', type: 'link', name: 'Partager', icon: 'share', desc: '' }
];
const MENULOGBOOK: Menu[] = [
{ state: '#', type: 'link', name: 'Recherche avancée', icon: 'search', desc: '' },
{ state: '#', type: 'link', name: 'Recherche par participant', icon: 'person_search', desc: '' },
{ state: '#', type: 'link', name: 'Paramètres d\'affichage', icon: 'display_settings', desc: '' },
{ state: '#', type: 'link', name: 'Import CSV', icon: 'upload_file', desc: '' }
];
@Injectable()
export class MenuItems {
getMenuAuth(): Menu[] {
return MENUAUTH;
}
getMenuCalculator(): Menu[] {
return MENUCALCULATOR;
}
getMenuItems(): Menu[] {
return MENUITEMS;
}
getMenuLogbook(): Menu[] {
return MENULOGBOOK;
}
getMenuQCM(): Menu[] {
return MENUQCM;
}
getMenuPanel(): Menu[] {
return MENUPANEL;
}
getMenuJump(): Menu[] {
return MENUPJUMP;
}
}
@@ -0,0 +1,45 @@
import { Directive, Input, OnInit, OnDestroy, TemplateRef, ViewContainerRef } from '@angular/core';
import { Observable, Subscription } from 'rxjs';
import { UserService } from 'src/app/core/services';
@Directive({
selector: '[appShowAuthed]',
standalone: true
})
export class ShowAuthedDirective implements OnInit, OnDestroy {
private _auth: Subscription = new Subscription();
constructor(
//private templateRef: TemplateRef<any>,
private templateRef: TemplateRef<never>,
private userService: UserService,
private viewContainer: ViewContainerRef
) { }
@Input() set appShowAuthed(condition: boolean) {
this.condition = condition;
}
condition: boolean = false;
//condition: boolean;
ngOnInit() {
const isAuthenticated$: Observable<boolean> = this.userService.isAuthenticated; // .pipe(take(1));
this._auth = isAuthenticated$.subscribe(
(isAuthenticated) => {
if (isAuthenticated && this.condition || !isAuthenticated && !this.condition) {
this.viewContainer.createEmbeddedView(this.templateRef);
} else {
this.viewContainer.clear();
}
}
);
}
ngOnDestroy() {
this._auth.unsubscribe();
}
}
@@ -0,0 +1,8 @@
@if (isSpinnerVisible) {
<div class="preloader">
<div class="spinner">
<div class="double-bounce1"></div>
<div class="double-bounce2"></div>
</div>
</div>
}
+44
View File
@@ -0,0 +1,44 @@
import { Component, Input, OnDestroy, Inject, ViewEncapsulation } from '@angular/core';
import { Router, NavigationStart, NavigationEnd, NavigationCancel, NavigationError } from '@angular/router';
import { DOCUMENT } from '@angular/common';
@Component({
selector: 'app-spinner',
standalone: true,
imports: [],
templateUrl: './spinner.component.html',
encapsulation: ViewEncapsulation.None
})
export class SpinnerComponent implements OnDestroy {
public isSpinnerVisible = true;
@Input()
public backgroundColor = 'rgba(0, 115, 170, 0.69)';
constructor(
private router: Router,
@Inject(DOCUMENT) private document: Document
) {
this.router.events.subscribe(
event => {
if (event instanceof NavigationStart) {
this.isSpinnerVisible = true;
} else if (
event instanceof NavigationEnd ||
event instanceof NavigationCancel ||
event instanceof NavigationError
) {
this.isSpinnerVisible = false;
}
},
() => {
this.isSpinnerVisible = false;
}
);
}
ngOnDestroy(): void {
this.isSpinnerVisible = false;
}
}