chore: merge angular branch into develop
Resolves add/add conflicts by keeping Angular-specific versions. Fixes pre-existing build issues surfaced by the merge: - Remove obsolete _icomoon.scss (conflict with icomoon.scss) - Replace deprecated async with waitForAsync in 6 spec files (Angular 18) - Add stylePreprocessorOptions.includePaths to test config in angular.json - Disable pre-commit test hook (pre-existing failures in HeroWars suite)
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
Executable
+3
@@ -0,0 +1,3 @@
|
||||
export * from './accordionanchor.directive';
|
||||
export * from './accordionlink.directive';
|
||||
export * from './accordion.directive';
|
||||
@@ -0,0 +1,16 @@
|
||||
<mat-card class="my-3">
|
||||
<mat-card-header class="{{ colors.background }} {{ colors.text }} d-flex rounded-top py-2 px-3">
|
||||
<div class="flex-fill">
|
||||
<mat-card-title>
|
||||
{{ title }}
|
||||
</mat-card-title>
|
||||
</div>
|
||||
<span class="flex-spacer"></span>
|
||||
<div class="align-self-center text-end">
|
||||
<span class="fs-5 font-light">{{ subtitle }}</span>
|
||||
</div>
|
||||
</mat-card-header>
|
||||
<mat-card-content class="table-responsive p-0">
|
||||
<ng-content></ng-content>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
@@ -0,0 +1,18 @@
|
||||
import { Component, Input } from '@angular/core';
|
||||
import { NgForOf, NgIf } from "@angular/common";
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
|
||||
import { CardColors } from 'src/app/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-card-container',
|
||||
templateUrl: './card-container.component.html',
|
||||
standalone: true,
|
||||
imports: [NgIf, NgForOf, MatCardModule]
|
||||
})
|
||||
export class CardContainerComponent {
|
||||
errorList: string[] = [];
|
||||
@Input() colors: CardColors = { background: 'bg-megna', text: 'text-bg-primary' };
|
||||
@Input() title: string = 'Title';
|
||||
@Input() subtitle: string = 'Subtitle';
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+42
@@ -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>
|
||||
Executable
+25
@@ -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();
|
||||
});
|
||||
});
|
||||
+95
@@ -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 '@components/shared';
|
||||
import { BarHorizontalChartComponent } from '@components/shared/helpers-chart';
|
||||
import { HistoryTableComponent } from '@components/shared/helpers-jump';
|
||||
import { UtilitiesService, AeronefsService } from '@services';
|
||||
import { AeronefByImat, AeronefByYear } from '@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;
|
||||
});
|
||||
}
|
||||
}
|
||||
+42
@@ -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>
|
||||
Executable
+25
@@ -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();
|
||||
});
|
||||
});
|
||||
+93
@@ -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 '@components/shared';
|
||||
import { HistoryTableComponent } from '@components/shared/helpers-jump';
|
||||
import { PieChartComponent } from '@components/shared/helpers-chart';
|
||||
import { UtilitiesService, AeronefsService } from '@services';
|
||||
import { AeronefByImat, AeronefByYear } from '@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;
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
Executable
+31
@@ -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>
|
||||
Executable
Executable
+25
@@ -0,0 +1,25 @@
|
||||
import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { CanopiesModelsComponent } from './canopies-models.component';
|
||||
|
||||
describe('CanopiesModelsComponent', () => {
|
||||
let component: CanopiesModelsComponent;
|
||||
let fixture: ComponentFixture<CanopiesModelsComponent>;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [CanopiesModelsComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(CanopiesModelsComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
Executable
+92
@@ -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 '@components/shared';
|
||||
import { PieChartComponent } from '@components/shared/helpers-chart';
|
||||
import { HistoryTableComponent } from '@components/shared/helpers-jump';
|
||||
import { UtilitiesService, CanopiesService } from '@services';
|
||||
import { CanopyModelBySize, CanopyModelByYear } from '@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;
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
Executable
+42
@@ -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>
|
||||
Executable
Executable
+25
@@ -0,0 +1,25 @@
|
||||
import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { CanopiesSizesComponent } from './canopies-sizes.component';
|
||||
|
||||
describe('CanopiesSizesComponent', () => {
|
||||
let component: CanopiesSizesComponent;
|
||||
let fixture: ComponentFixture<CanopiesSizesComponent>;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [CanopiesSizesComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(CanopiesSizesComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
Executable
+92
@@ -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 '@components/shared';
|
||||
import { PieChartComponent } from '@components/shared/helpers-chart';
|
||||
import { HistoryTableComponent } from '@components/shared/helpers-jump';
|
||||
import { UtilitiesService, CanopiesService } from '@services';
|
||||
import { CanopyBySize, CanopyByYear } from '@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"]
|
||||
}
|
||||
}
|
||||
Executable
+35
@@ -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>
|
||||
Executable
Executable
+25
@@ -0,0 +1,25 @@
|
||||
import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { DropzonesBarComponent } from './dropzones-bar.component';
|
||||
|
||||
describe('DropzonesBarComponent', () => {
|
||||
let component: DropzonesBarComponent;
|
||||
let fixture: ComponentFixture<DropzonesBarComponent>;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [DropzonesBarComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(DropzonesBarComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
+143
@@ -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 '@components/shared';
|
||||
import { HistoryTableComponent } from '@components/shared/helpers-jump';
|
||||
import { UtilitiesService, DropZonesService } from '@services';
|
||||
import { DropZoneByOaci, DropZoneByYear } from '@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"]
|
||||
}
|
||||
}
|
||||
Executable
+31
@@ -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>
|
||||
Executable
Executable
+25
@@ -0,0 +1,25 @@
|
||||
import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { DropzonesPieComponent } from './dropzones-pie.component';
|
||||
|
||||
describe('DropzonesPieComponent', () => {
|
||||
let component: DropzonesPieComponent;
|
||||
let fixture: ComponentFixture<DropzonesPieComponent>;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [DropzonesPieComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(DropzonesPieComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
+93
@@ -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 '@components/shared';
|
||||
import { PieChartComponent } from '@components/shared/helpers-chart';
|
||||
import { HistoryTableComponent } from '@components/shared/helpers-jump';
|
||||
import { UtilitiesService, DropZonesService } from '@services';
|
||||
import { DropZoneByOaci, DropZoneByYear } from '@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]
|
||||
}
|
||||
}
|
||||
Executable
+86
@@ -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>
|
||||
Executable
Executable
+25
@@ -0,0 +1,25 @@
|
||||
import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { JumpsByMonthComponent } from './jumps-by-month.component';
|
||||
|
||||
describe('JumpsByMonthComponent', () => {
|
||||
let component: JumpsByMonthComponent;
|
||||
let fixture: ComponentFixture<JumpsByMonthComponent>;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [JumpsByMonthComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(JumpsByMonthComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
Executable
+172
@@ -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 '@components/shared';
|
||||
import { UtilitiesService, JumpsService } from '@services';
|
||||
import { JumpByDate } from '@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,10 @@
|
||||
import { Pipe, PipeTransform } from '@angular/core';
|
||||
@Pipe({
|
||||
name: 'floor',
|
||||
standalone: true
|
||||
})
|
||||
export class FloorPipe implements PipeTransform {
|
||||
transform(value: number): number {
|
||||
return Math.floor(value);
|
||||
}
|
||||
}
|
||||
@@ -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 '@services';
|
||||
import { BarConfig } from '@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;
|
||||
}
|
||||
}
|
||||
+11
@@ -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>
|
||||
}
|
||||
+25
@@ -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 '@services';
|
||||
import { BarConfig } from '@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 '@services';
|
||||
import { BarConfig } from '@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 '@services';
|
||||
import { CircleConfig } from '@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 '@services';
|
||||
import { LineConfig } from '@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 '@services';
|
||||
import { LineConfig } from '@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 '@services';
|
||||
import { DoughnutConfig } from '@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 '@models';
|
||||
import { JumpsService } from '@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 '@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 '@models';
|
||||
import { JumpsService } from '@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 '@components/shared/french-paginator.component';
|
||||
import { ColumnDefinition, Errors, Jump, JumpByDay, JumpFile, JumpFileType, JumpList, JumpListConfig, jumpColumns } from '@models';
|
||||
import { JumpsService, JumpTableService } from '@services';
|
||||
import { JumpAddDialogComponent, JumpDeleteDialogComponent, JumpEditDialogComponent, JumpViewDialogComponent } from '@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,
|
||||
}))
|
||||
}
|
||||
*/
|
||||
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
<mat-card-subtitle>
|
||||
<span class="border-bottom border-navy text-navy fw-normal fs-5">{{ data.name }}</span>
|
||||
<small class="font-monospace text-navy float-end">[ {{ data.count | number : '1.0' }} ]</small>
|
||||
</mat-card-subtitle>
|
||||
<ul class="list-unstyled mt-2">
|
||||
@for (champion of data.teams; track champion; let index = $index) {
|
||||
<li [class]="(champion.id !== data.positions[data.slots[(index)]].toString()) ? 'text-red' : ''">
|
||||
<span class="slot">slot {{data.slots[(index)]}}</span>
|
||||
@if (champion.id !== data.positions[data.slots[(index)]].toString()) {
|
||||
<span class="text-decoration-line-through">{{ getMemberNameById(data.positions[data.slots[(index)]].toString()) }}</span>
|
||||
<mat-icon fontIcon="swap_horiz" class="mx-3 align-middle icon-xsmall"></mat-icon>
|
||||
}
|
||||
<span>{{ champion.name }}</span>
|
||||
<span class="font-monospace float-end">
|
||||
@switch (teamType) {
|
||||
@case ('heroes') {
|
||||
@if (champion.id !== data.positions[data.slots[(index)]].toString()) {
|
||||
<span class="text-decoration-line-through">{{ getMemberHeroesPowerById(data.positions[data.slots[(index)]].toString()) | number : '1.0' }}</span>
|
||||
<mat-icon fontIcon="swap_horiz" class="mx-3 align-middle icon-xsmall"></mat-icon>
|
||||
}
|
||||
{{ champion.heroes.power | number : '1.0' }}
|
||||
}
|
||||
@case ('titans') {
|
||||
@if (champion.id !== data.positions[data.slots[(index)]].toString()) {
|
||||
<span class="text-decoration-line-through">{{ getMemberTitansPowerById(data.positions[data.slots[(index)]].toString()) | number : '1.0' }}</span>
|
||||
<mat-icon fontIcon="swap_horiz" class="mx-3 align-middle icon-xsmall"></mat-icon>
|
||||
}
|
||||
{{ champion.titans.power | number : '1.0' }}
|
||||
}
|
||||
@default {
|
||||
0
|
||||
}
|
||||
}
|
||||
</span>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
.slot {
|
||||
min-width: 60px;
|
||||
display: inline-block;
|
||||
}
|
||||
.mat-mdc-standard-chip {
|
||||
--mdc-chip-container-height: 28px;
|
||||
--mdc-chip-container-shape-radius: 8px 8px 8px 8px;
|
||||
--mdc-chip-label-text-color: inherit;
|
||||
--mdc-chip-label-text-line-height: 20px;
|
||||
--mdc-chip-label-text-size: 13px;
|
||||
--mdc-chip-label-text-weight: 400;
|
||||
--mdc-chip-with-avatar-avatar-shape-radius: 6px 6px 6px 6px;
|
||||
}
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
import { Component, Input } from '@angular/core';
|
||||
import { DecimalPipe } from '@angular/common';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MatChipsModule } from '@angular/material/chips';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
|
||||
//import { Errors } from '@models';
|
||||
//import { HWMember, HWGuildWarFortification, HWGuildWarHeroTeam, HWGuildWarTitanTeam } from '@models';
|
||||
import { HWGuildWarFortification, HWMember } from '@models';
|
||||
|
||||
@Component({
|
||||
selector: 'app-fortification-card-content',
|
||||
templateUrl: './fortification-card-content.component.html',
|
||||
styleUrl: './fortification-card-content.component.scss',
|
||||
standalone: true,
|
||||
imports: [
|
||||
DecimalPipe,
|
||||
MatCardModule, MatChipsModule, MatIconModule
|
||||
]
|
||||
})
|
||||
export class FortificationCardContentComponent {
|
||||
public data: HWGuildWarFortification = {} as HWGuildWarFortification;
|
||||
public teamType: string = '';
|
||||
public guildMembers: HWMember[] = [];
|
||||
//public errorList: string[] = [];
|
||||
|
||||
@Input() set fortification(data: HWGuildWarFortification) {
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
@Input() set type(value: string) {
|
||||
this.teamType = value;
|
||||
}
|
||||
|
||||
@Input() set members(value: HWMember[]) {
|
||||
this.guildMembers = value;
|
||||
}
|
||||
|
||||
getMemberNameById(id: string): string {
|
||||
let name = '';
|
||||
this.guildMembers.some((item) => {
|
||||
if (item.id === id) {
|
||||
name = item.name;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
return name;
|
||||
}
|
||||
|
||||
getMemberHeroesPowerById(id: string): number {
|
||||
let power = 0;
|
||||
this.guildMembers.some((item) => {
|
||||
if (item.id === id) {
|
||||
power = item.heroes.power;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
return power;
|
||||
}
|
||||
|
||||
getMemberTitansPowerById(id: string): number {
|
||||
let power = 0;
|
||||
this.guildMembers.some((item) => {
|
||||
if (item.id === id) {
|
||||
power = item.titans.power;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
return power;
|
||||
}
|
||||
/*
|
||||
@Input() set errors(errorList: Errors | null) {
|
||||
this.errorList = errorList
|
||||
? Object.keys(errorList.errors || {}).map(
|
||||
(key) => `${key} ${errorList.errors[key]}`,
|
||||
)
|
||||
: [];
|
||||
}
|
||||
*/
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
<mat-card class="mb-3">
|
||||
<mat-card-header class="bg-raspberry text-bg-primary d-flex rounded-top py-2 px-3">
|
||||
<div class="flex-fill">
|
||||
<mat-card-title>
|
||||
{{ guildData.clan.title }} <span class="ms-2 me-2 fw-light opacity-75">|</span><small class="fs-5 fw-light opacity-75">server {{ guildData.clan.serverId }}</small>
|
||||
</mat-card-title>
|
||||
</div>
|
||||
<span class="flex-spacer"></span>
|
||||
<div class="align-self-center text-end">
|
||||
<span class="fs-5 font-light">{{ guildData.clan.membersCount }}/30</span>
|
||||
</div>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
<div class="row mt-2">
|
||||
<div class="col-xs-12 col-md-6">
|
||||
<span class="border-bottom border-raspberry text-raspberry fs-5">Server/Season Dates</span>
|
||||
<dl class="dl-horizontal dl-large mt-3 mb-0">
|
||||
<dt>Server Reset Time</dt>
|
||||
<dd class="font-monospace">{{ (guildData.serverResetTime*1000) | date: 'dd/MM/yyyy HH:mm:ss' }}</dd>
|
||||
<dt>War End Season Time</dt>
|
||||
<dd class="font-monospace">{{ (guildData.clanWarEndSeasonTime*1000) | date: 'dd/MM/yyyy HH:mm:ss' }}</dd>
|
||||
<dt>Free Clan Change Start</dt>
|
||||
<dd class="font-monospace">{{ (guildData.freeClanChangeInterval.start * 1000) | date: 'dd/MM/yyyy HH:mm:ss' }}</dd>
|
||||
<dt>Free Clan Change End</dt>
|
||||
<dd class="font-monospace">{{ (guildData.freeClanChangeInterval.end * 1000) | date: 'dd/MM/yyyy HH:mm:ss' }}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
<div class="col-xs-12 col-md-6">
|
||||
<span class="border-bottom border-raspberry text-raspberry fs-5">Guild Overview</span>
|
||||
<dl class="dl-horizontal dl-large mt-3 mb-0">
|
||||
<dt>Members</dt>
|
||||
<dd class="font-monospace text-end">{{ guildData.clan.membersCount }}/30</dd>
|
||||
<dt>Champions</dt>
|
||||
<dd class="font-monospace text-end">{{ guildData.clan.warriors.length }}/20</dd>
|
||||
<dt>Top Activity</dt>
|
||||
<dd class="font-monospace text-end">{{ guildData.clan.topActivity | number : '1.0' }}</dd>
|
||||
<dt>Top Dungeon</dt>
|
||||
<dd class="font-monospace text-end">{{ guildData.clan.topDungeon | number : '1.0' }}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
<mat-divider class="my-2"></mat-divider>
|
||||
<div class="row">
|
||||
<div class="col-xs-12 col-md-6">
|
||||
<span class="border-bottom border-raspberry text-raspberry fs-5">Guild Settings</span>
|
||||
<dl class="dl-horizontal dl-large mt-2 mb-0">
|
||||
<dt>Guild Min. Level</dt>
|
||||
<dd class="font-monospace">{{ guildData.clan.minLevel }}</dd>
|
||||
<dt>Dismissal Delay</dt>
|
||||
<dd class="font-monospace">{{ guildData.clan.daysToKick }} days</dd>
|
||||
<dt>Guild Master Gifts</dt>
|
||||
<dd class="font-monospace">{{ guildData.clan.giftsCount }}</dd>
|
||||
<dt>Adventure Stat</dt>
|
||||
<dd class="font-monospace">{{ guildData.stat.adventureStat }}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
<div class="col-xs-12 col-md-6">
|
||||
<span class="border-bottom border-raspberry text-raspberry fs-5">Solide Overview</span>
|
||||
<dl class="dl-horizontal dl-large mt-2 mb-0">
|
||||
<dt>Today Activity</dt>
|
||||
<dd class="font-monospace text-end">{{ guildData.stat.todayActivity }}</dd>
|
||||
<dt>Today Titanite</dt>
|
||||
<dd class="font-monospace text-end">{{ guildData.stat.todayDungeonActivity }}</dd>
|
||||
<dt>Activity Sum</dt>
|
||||
<dd class="font-monospace text-end">{{ guildData.stat.activitySum }}</dd>
|
||||
<dt>Titanite Sum</dt>
|
||||
<dd class="font-monospace text-end">{{ guildData.stat.dungeonActivitySum }}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { GuildCardComponent } from './guild-card.component';
|
||||
|
||||
describe('GuildCardComponent', () => {
|
||||
let component: GuildCardComponent;
|
||||
let fixture: ComponentFixture<GuildCardComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [GuildCardComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(GuildCardComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,36 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { DatePipe, DecimalPipe } from '@angular/common';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MatDividerModule } from '@angular/material/divider';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
|
||||
import { HWGuildData, HWGuildClan, HWGuildStat } from '@models';
|
||||
|
||||
import guildData from 'src/files-data/hw-guild-data.json'; // page Membres
|
||||
|
||||
@Component({
|
||||
selector: 'app-guild-card',
|
||||
standalone: true,
|
||||
imports: [
|
||||
DatePipe, DecimalPipe,
|
||||
MatCardModule, MatDividerModule, MatIconModule
|
||||
],
|
||||
templateUrl: './guild-card.component.html',
|
||||
styleUrl: './guild-card.component.scss'
|
||||
})
|
||||
export class GuildCardComponent implements OnInit {
|
||||
public guildData: HWGuildData = {} as HWGuildData;
|
||||
private _guildClan: HWGuildClan = {} as HWGuildClan;
|
||||
private _guildStat: HWGuildStat = {} as HWGuildStat;
|
||||
|
||||
ngOnInit() {
|
||||
Object.assign(this._guildClan, guildData.clan);
|
||||
this.guildData.clan = this._guildClan;
|
||||
Object.assign(this._guildStat, guildData.stat);
|
||||
this.guildData.stat = this._guildStat;
|
||||
this.guildData.serverResetTime = guildData.serverResetTime;
|
||||
this.guildData.clanWarEndSeasonTime = guildData.clanWarEndSeasonTime;
|
||||
this.guildData.freeClanChangeInterval = guildData.freeClanChangeInterval;
|
||||
this.guildData.giftUids = guildData.giftUids;
|
||||
}
|
||||
}
|
||||
+436
@@ -0,0 +1,436 @@
|
||||
<mat-card class="my-3">
|
||||
<mat-card-header class="d-flex rounded-top bg-navy-light text-bg-navy-light py-2 px-3">
|
||||
<div class="flex-fill">
|
||||
<mat-card-title>
|
||||
{{ title }}
|
||||
</mat-card-title>
|
||||
</div>
|
||||
<span class="flex-spacer"></span>
|
||||
<div class="align-self-center text-end">
|
||||
<span class="fs-5 font-light">{{ subtitle }}</span>
|
||||
</div>
|
||||
</mat-card-header>
|
||||
<mat-card-content class="table-responsive p-0">
|
||||
<form class="d-flex" [formGroup]="raidsForm" (ngSubmit)="submitForm()">
|
||||
<mat-form-field appearance="outline" class="mt-2 me-3" color="warn">
|
||||
<mat-label>Choisissez une date</mat-label>
|
||||
<input matInput [matDatepicker]="datepicker" formControlName="startTime" (dateChange)="onDateChange()" required
|
||||
cdkFocusInitial />
|
||||
<mat-hint>MM/DD/YYYY</mat-hint>
|
||||
<mat-datepicker-toggle matIconSuffix [for]="datepicker"></mat-datepicker-toggle>
|
||||
<mat-datepicker #datepicker>
|
||||
<mat-datepicker-actions>
|
||||
<button mat-button matDatepickerCancel>Annuler</button>
|
||||
<button mat-raised-button color="warn" matDatepickerApply>Appliquer</button>
|
||||
</mat-datepicker-actions>
|
||||
</mat-datepicker>
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="outline" class="mt-2 me-2" color="warn">
|
||||
<mat-label>Duration</mat-label>
|
||||
<input matInput formControlName="duration" required />
|
||||
<mat-hint>Stage duration in hour</mat-hint>
|
||||
</mat-form-field>
|
||||
</form>
|
||||
<mat-stepper headerPosition="top" #stepper>
|
||||
<mat-step state="timeline">
|
||||
<ng-template matStepLabel>Stage 1</ng-template>
|
||||
<div class="d-flex mt-3">
|
||||
<div class="flex-fill">
|
||||
<h2 class="mb-1">Stage 1</h2>
|
||||
<h4 class="mb-1">
|
||||
From {{ getStagesTime(0).startTime | date: 'dd/MM HH:mm' }}
|
||||
to {{ getStagesTime(0).endTime | date: 'dd/MM HH:mm' }}
|
||||
</h4>
|
||||
</div>
|
||||
<span class="flex-spacer"></span>
|
||||
<div class="align-self-center me-2 text-end">
|
||||
<h4 class="mb-1">Power limit: 320k</h4>
|
||||
</div>
|
||||
</div>
|
||||
<table class="table table-striped table-condensed">
|
||||
<thead>
|
||||
<tr role="row" class="bg-gray-500 text-bg-primary">
|
||||
<th scope="col" class="align-middle w-auto ps-4">
|
||||
<div class="text-start">
|
||||
#
|
||||
</div>
|
||||
</th>
|
||||
<th scope="col" class="align-middle w-auto">
|
||||
<div class="d-flex align-items-center justify-content-end">
|
||||
Date
|
||||
<mat-icon fontIcon="arrow_upward" class="ms-1 icon-xsmall"></mat-icon>
|
||||
</div>
|
||||
</th>
|
||||
<th scope="col" class="align-middle w-auto">
|
||||
<div class="text-end">
|
||||
Member
|
||||
</div>
|
||||
</th>
|
||||
<th scope="col" class="align-middle w-auto">
|
||||
<div class="text-end">
|
||||
Level
|
||||
</div>
|
||||
</th>
|
||||
<th scope="col" class="align-middle w-auto">
|
||||
<div class="text-end">
|
||||
Damage
|
||||
</div>
|
||||
</th>
|
||||
<th scope="col" class="align-middle w-auto">
|
||||
<div class="text-end">
|
||||
Stage
|
||||
</div>
|
||||
</th>
|
||||
<th scope="col" class="align-middle w-auto">
|
||||
<div class="text-end">
|
||||
Power
|
||||
</div>
|
||||
</th>
|
||||
<th scope="col" class="align-middle w-auto pe-4">
|
||||
<div class="text-end">
|
||||
Variation
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (raid of getRaidsLog(1); track raid; let index = $index) {
|
||||
<tr role="row" class="align-middle text-{{ raid.variation.color }}">
|
||||
<td class="cell text-start ps-4">
|
||||
{{ (index+1) }}
|
||||
</td>
|
||||
<td class="cell font-monospace text-end">
|
||||
<span class="me-3">{{ raid.startTime + '000' | date: 'dd/MM HH:mm:ss' }}</span>
|
||||
</td>
|
||||
<td class="cell font-monospace text-end">
|
||||
{{ raid.userName }}
|
||||
</td>
|
||||
<td class="cell font-monospace text-end {{ raid.damage['2'] > 0 ? 'text-purple' : '' }}">
|
||||
{{ raid.level | number : '1.0' }}
|
||||
</td>
|
||||
<td class="cell font-monospace text-end {{ raid.damage['2'] > 0 ? 'text-purple' : '' }}">
|
||||
<div class="d-flex align-items-center justify-content-end">
|
||||
@if (raid.damage["2"] > 0) {
|
||||
<mat-icon fontIcon="arrow_upward" color="purple" class="me-2 icon-xxsmall" matTooltip="{{ raid.damage['1'] | number : '1.0' }} + {{ raid.damage['2'] | number : '1.0' }}"></mat-icon>
|
||||
}
|
||||
{{ (raid.damage["1"] + raid.damage["2"]) | number : '1.0' }}
|
||||
</div>
|
||||
</td>
|
||||
<td class="cell font-monospace text-end">
|
||||
{{ raid.stage.name }}: {{ (raid.stage.maxPower / 1000) | number : '1.0' }}k
|
||||
</td>
|
||||
<td class="cell font-monospace text-end">
|
||||
{{ raid.power | number : '1.0' }}
|
||||
</td>
|
||||
<td class="cell font-monospace text-end pe-4">
|
||||
<span class="ms-3" [matTooltip]="raid.tooltip">
|
||||
{{ raid.variation.percent | number : '1.0-2' }}%
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="border-top border-light pt-2">
|
||||
<button mat-button matStepperPrevious>Back</button>
|
||||
<button mat-button matStepperNext>Next</button>
|
||||
</div>
|
||||
</mat-step>
|
||||
<mat-step state="timeline">
|
||||
<ng-template matStepLabel>Stage 2</ng-template>
|
||||
<div class="d-flex mt-3">
|
||||
<div class="flex-fill">
|
||||
<h2 class="mb-1">Stage 2</h2>
|
||||
<h4 class="mb-1">
|
||||
From {{ getStagesTime(1).startTime | date: 'dd/MM HH:mm' }}
|
||||
to {{ getStagesTime(1).endTime | date: 'dd/MM HH:mm' }}
|
||||
</h4>
|
||||
</div>
|
||||
<span class="flex-spacer"></span>
|
||||
<div class="align-self-center me-2 text-end">
|
||||
<h4 class="mb-1">Power limit: 500k</h4>
|
||||
</div>
|
||||
</div>
|
||||
<table class="table table-striped table-condensed">
|
||||
<thead>
|
||||
<tr role="row" class="bg-gray-500 text-bg-primary">
|
||||
<th scope="col" class="align-middle w-auto ps-4">
|
||||
<div class="text-start">
|
||||
#
|
||||
</div>
|
||||
</th>
|
||||
<th scope="col" class="align-middle w-auto">
|
||||
<div class="d-flex align-items-center justify-content-end">
|
||||
Date
|
||||
<mat-icon fontIcon="arrow_upward" class="ms-1 icon-xsmall"></mat-icon>
|
||||
</div>
|
||||
</th>
|
||||
<th scope="col" class="align-middle w-auto">
|
||||
<div class="text-end">
|
||||
Member
|
||||
</div>
|
||||
</th>
|
||||
<th scope="col" class="align-middle w-auto">
|
||||
<div class="text-end">
|
||||
Level
|
||||
</div>
|
||||
</th>
|
||||
<th scope="col" class="align-middle w-auto">
|
||||
<div class="text-end">
|
||||
Damage
|
||||
</div>
|
||||
</th>
|
||||
<th scope="col" class="align-middle w-auto">
|
||||
<div class="text-end">
|
||||
Stage
|
||||
</div>
|
||||
</th>
|
||||
<th scope="col" class="align-middle w-auto">
|
||||
<div class="text-end">
|
||||
Power
|
||||
</div>
|
||||
</th>
|
||||
<th scope="col" class="align-middle w-auto pe-4">
|
||||
<div class="text-end">
|
||||
Variation
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (raid of getRaidsLog(2); track raid; let index = $index) {
|
||||
<tr role="row" class="align-middle text-{{ raid.variation.color }}">
|
||||
<td class="cell text-start ps-4">
|
||||
{{ (index+1) }}
|
||||
</td>
|
||||
<td class="cell font-monospace text-end">
|
||||
<span class="me-3">{{ raid.startTime + '000' | date: 'dd/MM HH:mm:ss' }}</span>
|
||||
</td>
|
||||
<td class="cell font-monospace text-end">
|
||||
{{ raid.userName }}
|
||||
</td>
|
||||
<td class="cell font-monospace text-end {{ raid.damage['2'] > 0 ? 'text-purple' : '' }}">
|
||||
{{ raid.level | number : '1.0' }}
|
||||
</td>
|
||||
<td class="cell font-monospace text-end {{ raid.damage['2'] > 0 ? 'text-purple' : '' }}">
|
||||
<div class="d-flex align-items-center justify-content-end">
|
||||
@if (raid.damage["2"] > 0) {
|
||||
<mat-icon fontIcon="arrow_upward" color="purple" class="me-2 icon-xxsmall" matTooltip="{{ raid.damage['1'] | number : '1.0' }} + {{ raid.damage['2'] | number : '1.0' }}"></mat-icon>
|
||||
}
|
||||
{{ (raid.damage["1"] + raid.damage["2"]) | number : '1.0' }}
|
||||
</div>
|
||||
</td>
|
||||
<td class="cell font-monospace text-end">
|
||||
{{ raid.stage.name }}: {{ (raid.stage.maxPower / 1000) | number : '1.0' }}k
|
||||
</td>
|
||||
<td class="cell font-monospace text-end">
|
||||
{{ raid.power | number : '1.0' }}
|
||||
</td>
|
||||
<td class="cell font-monospace text-end pe-4">
|
||||
<span class="ms-3" [matTooltip]="raid.tooltip">
|
||||
{{ raid.variation.percent | number : '1.0-2' }}%
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="border-top border-light pt-2">
|
||||
<button mat-button matStepperPrevious>Back</button>
|
||||
<button mat-button matStepperNext>Next</button>
|
||||
</div>
|
||||
</mat-step>
|
||||
<mat-step state="timeline">
|
||||
<ng-template matStepLabel>Stage 3</ng-template>
|
||||
<div class="d-flex mt-3">
|
||||
<div class="flex-fill">
|
||||
<h2 class="mb-1">Stage 3</h2>
|
||||
<h4 class="mb-1">
|
||||
From {{ getStagesTime(2).startTime | date: 'dd/MM HH:mm' }}
|
||||
to {{ getStagesTime(2).endTime | date: 'dd/MM HH:mm' }}
|
||||
</h4>
|
||||
</div>
|
||||
<span class="flex-spacer"></span>
|
||||
<div class="align-self-center me-2 text-end">
|
||||
<h4 class="mb-1">No power limit</h4>
|
||||
</div>
|
||||
</div>
|
||||
<table class="table table-striped table-condensed">
|
||||
<thead>
|
||||
<tr role="row" class="bg-gray-500 text-bg-primary">
|
||||
<th scope="col" class="align-middle w-auto ps-4">
|
||||
<div class="text-start">
|
||||
#
|
||||
</div>
|
||||
</th>
|
||||
<th scope="col" class="align-middle w-auto">
|
||||
<div class="d-flex align-items-center justify-content-end">
|
||||
Date
|
||||
<mat-icon fontIcon="arrow_upward" class="ms-1 icon-xsmall"></mat-icon>
|
||||
</div>
|
||||
</th>
|
||||
<th scope="col" class="align-middle w-auto">
|
||||
<div class="text-end">
|
||||
Member
|
||||
</div>
|
||||
</th>
|
||||
<th scope="col" class="align-middle w-auto">
|
||||
<div class="text-end">
|
||||
Level
|
||||
</div>
|
||||
</th>
|
||||
<th scope="col" class="align-middle w-auto">
|
||||
<div class="text-end">
|
||||
Damage
|
||||
</div>
|
||||
</th>
|
||||
<th scope="col" class="align-middle w-auto">
|
||||
<div class="text-end">
|
||||
Stage
|
||||
</div>
|
||||
</th>
|
||||
<th scope="col" class="align-middle w-auto">
|
||||
<div class="text-end">
|
||||
Power
|
||||
</div>
|
||||
</th>
|
||||
<th scope="col" class="align-middle w-auto pe-4">
|
||||
<div class="text-end">
|
||||
Variation
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (raid of getRaidsLog(3); track raid; let index = $index) {
|
||||
<tr role="row" class="align-middle text-{{ raid.variation.color }}">
|
||||
<td class="cell text-start ps-4">
|
||||
{{ (index+1) }}
|
||||
</td>
|
||||
<td class="cell font-monospace text-end">
|
||||
<span class="me-3">{{ raid.startTime + '000' | date: 'dd/MM HH:mm:ss' }}</span>
|
||||
</td>
|
||||
<td class="cell font-monospace text-end">
|
||||
{{ raid.userName }}
|
||||
</td>
|
||||
<td class="cell font-monospace text-end {{ raid.damage['2'] > 0 ? 'text-purple' : '' }}">
|
||||
{{ raid.level | number : '1.0' }}
|
||||
</td>
|
||||
<td class="cell font-monospace text-end {{ raid.damage['2'] > 0 ? 'text-purple' : '' }}">
|
||||
<div class="d-flex align-items-center justify-content-end">
|
||||
@if (raid.damage["2"] > 0) {
|
||||
<mat-icon fontIcon="arrow_upward" color="purple" class="me-2 icon-xxsmall" matTooltip="{{ raid.damage['1'] | number : '1.0' }} + {{ raid.damage['2'] | number : '1.0' }}"></mat-icon>
|
||||
}
|
||||
{{ (raid.damage["1"] + raid.damage["2"]) | number : '1.0' }}
|
||||
</div>
|
||||
</td>
|
||||
<td class="cell font-monospace text-end">
|
||||
{{ raid.stage.name }}: {{ (raid.stage.maxPower / 1000) | number : '1.0' }}k
|
||||
</td>
|
||||
<td class="cell font-monospace text-end">
|
||||
{{ raid.power | number : '1.0' }}
|
||||
</td>
|
||||
<td class="cell font-monospace text-end pe-4">
|
||||
<span class="ms-3" [matTooltip]="raid.tooltip">
|
||||
{{ raid.variation.percent | number : '1.0-2' }}%
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="border-top border-light pt-2">
|
||||
<button mat-button matStepperPrevious>Back</button>
|
||||
<button mat-button matStepperNext>Next</button>
|
||||
</div>
|
||||
</mat-step>
|
||||
<mat-step state="search">
|
||||
<ng-template matStepLabel>Statistics</ng-template>
|
||||
<div class="d-flex mt-3">
|
||||
<div class="flex-fill">
|
||||
<h2 class="mb-1">Statistics</h2>
|
||||
</div>
|
||||
<span class="flex-spacer"></span>
|
||||
<div class="align-self-center me-2 text-end">
|
||||
<h4 class="mb-1">variation ≥ {{ minVarPct }}%</h4>
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Variation</mat-label>
|
||||
<input matInput [(ngModel)]="minVarPct" type="number" min="0">
|
||||
<mat-icon matSuffix>percent</mat-icon>
|
||||
<mat-hint>excess power in %</mat-hint>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
<table class="table table-striped table-condensed">
|
||||
<thead>
|
||||
<tr role="row" class="bg-gray-500 text-bg-primary">
|
||||
<th scope="col" class="align-middle w-auto ps-4">
|
||||
<div class="text-start">
|
||||
#
|
||||
</div>
|
||||
</th>
|
||||
<th scope="col" class="align-middle w-auto">
|
||||
<div class="text-end">
|
||||
Name
|
||||
</div>
|
||||
</th>
|
||||
<th scope="col" class="align-middle w-auto">
|
||||
<div class="text-end">
|
||||
Fights
|
||||
</div>
|
||||
</th>
|
||||
<th scope="col" class="align-middle w-auto">
|
||||
<div class="d-flex align-items-center justify-content-end">
|
||||
Variation moyenne
|
||||
<mat-icon fontIcon="arrow_upward" class="ms-1 icon-xsmall"></mat-icon>
|
||||
</div>
|
||||
</th>
|
||||
<th scope="col" class="align-middle w-auto pe-4">
|
||||
<div class="text-start ps-2">
|
||||
[]
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (member of getMembers(); track member; let index = $index) {
|
||||
<tr role="row" class="align-middle">
|
||||
<td class="cell text-start ps-4">
|
||||
{{ (index+1) }}
|
||||
</td>
|
||||
<td class="cell font-monospace text-end">
|
||||
{{ member.name }}
|
||||
</td>
|
||||
<td class="cell font-monospace text-end">
|
||||
{{ member.raids.length }}
|
||||
</td>
|
||||
<td class="cell font-monospace text-end">
|
||||
{{ member.raidsInfo.variationAvg | number : '1.0-2' }}%
|
||||
</td>
|
||||
<td class="cell font-monospace text-end">
|
||||
<div class="d-flex justify-content-start">
|
||||
@for (raid of member.raids; track raid) {
|
||||
<div class="px-2 text-{{ raid.variation.color }}">
|
||||
{{ raid.variation.percent | number : '1.2-2' }}%
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="border-top border-light pt-2">
|
||||
<button mat-button matStepperPrevious>Back</button>
|
||||
<button mat-button (click)="stepper.reset()">Reset</button>
|
||||
</div>
|
||||
</mat-step>
|
||||
<ng-template matStepperIcon="timeline">
|
||||
<mat-icon>timeline</mat-icon>
|
||||
</ng-template>
|
||||
<ng-template matStepperIcon="search">
|
||||
<mat-icon>manage_search</mat-icon>
|
||||
</ng-template>
|
||||
</mat-stepper>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
+1
@@ -0,0 +1 @@
|
||||
/* guildraids-log */
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { GuildraidsLogComponent } from './guildraids-log.component';
|
||||
|
||||
describe('GuildraidsLogComponent', () => {
|
||||
let component: GuildraidsLogComponent;
|
||||
let fixture: ComponentFixture<GuildraidsLogComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [GuildraidsLogComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(GuildraidsLogComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
+214
@@ -0,0 +1,214 @@
|
||||
import { Component, Injectable, inject, Input, OnInit } from '@angular/core';
|
||||
import { DatePipe, DecimalPipe } from '@angular/common';
|
||||
import { FormBuilder, FormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||
import { STEPPER_GLOBAL_OPTIONS } from '@angular/cdk/stepper';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatDatepickerModule } from '@angular/material/datepicker';
|
||||
import { MatDividerModule } from '@angular/material/divider';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatStepperIntl, MatStepperModule } from '@angular/material/stepper';
|
||||
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||
|
||||
import { HWGuildRaid, HWGuildRaidAttacker, HWGuildRaidAttackers, HWGuildRaidStage, HWMember } from '@models';
|
||||
|
||||
import guildRaids from 'src/files-data/hw-guild-raids.json'; // page Asgard -> Guild Raid -> Log
|
||||
|
||||
@Injectable()
|
||||
export class StepperIntl extends MatStepperIntl {
|
||||
// the default optional label text, if unspecified is "Optional"
|
||||
override optionalLabel = 'Optional Label';
|
||||
}
|
||||
@Component({
|
||||
selector: 'app-guildraids-log',
|
||||
standalone: true,
|
||||
providers: [
|
||||
{
|
||||
provide: STEPPER_GLOBAL_OPTIONS,
|
||||
useValue: {displayDefaultIndicatorType: false},
|
||||
},
|
||||
],
|
||||
imports: [
|
||||
DatePipe, DecimalPipe, FormsModule, ReactiveFormsModule,
|
||||
MatButtonModule, MatCardModule, MatDatepickerModule, MatDividerModule,
|
||||
MatFormFieldModule, MatIconModule, MatInputModule, MatTooltipModule,
|
||||
MatStepperModule
|
||||
],
|
||||
templateUrl: './guildraids-log.component.html',
|
||||
styleUrl: './guildraids-log.component.scss'
|
||||
})
|
||||
export class GuildraidsLogComponent implements OnInit {
|
||||
public now: Date;
|
||||
public raidsForm: FormGroup;
|
||||
public guildMembers: HWMember[] = [];
|
||||
public title = 'Guild Raids';
|
||||
public subtitle = 'Log';
|
||||
public minVarPct = 3;
|
||||
private _matStepperIntl = inject(MatStepperIntl);
|
||||
private _guildRaids: HWGuildRaid[] = [];
|
||||
private _raidStages: HWGuildRaidStage[] = [
|
||||
{num: 1, name: 'Stage 1', maxPower: 350000, duration: 8, startTime: 0, endTime: 0},
|
||||
{num: 2, name: 'Stage 2', maxPower: 550000, duration: 8, startTime: 0, endTime: 0},
|
||||
{num: 3, name: 'Stage 3', maxPower: 0, duration: 0, startTime: 0, endTime: 0}
|
||||
];
|
||||
|
||||
@Input() set members(value: HWMember[]) {
|
||||
this.guildMembers = value;
|
||||
}
|
||||
|
||||
constructor(
|
||||
private fb: FormBuilder
|
||||
) {
|
||||
this.now = new Date();
|
||||
const raidsDate = new Date();
|
||||
const startHour = 4;
|
||||
raidsDate.setHours(startHour, 0, 0, 0);
|
||||
const controlsConfig = {
|
||||
num: 1,
|
||||
name: 'Stage 1',
|
||||
maxPower: 350,
|
||||
duration: 8,
|
||||
startTime: [raidsDate.getTime(), Validators.required],
|
||||
endTime: 0
|
||||
};
|
||||
this.raidsForm = this.fb.group(controlsConfig);
|
||||
}
|
||||
|
||||
_setStagesTime(startTime: number): void {
|
||||
const raidsDate = new Date(startTime);
|
||||
const startHour = 4;
|
||||
raidsDate.setHours(startHour, 0, 0, 0);
|
||||
this._raidStages[0].startTime = raidsDate.getTime();
|
||||
|
||||
raidsDate.setHours((startHour + this._raidStages[0].duration), 0, 0, 0);
|
||||
this._raidStages[0].endTime = raidsDate.getTime();
|
||||
this._raidStages[1].startTime = raidsDate.getTime();
|
||||
|
||||
raidsDate.setHours((startHour + this._raidStages[0].duration + this._raidStages[1].duration), 0, 0, 0);
|
||||
this._raidStages[1].endTime = raidsDate.getTime();
|
||||
this._raidStages[2].startTime = raidsDate.getTime();
|
||||
|
||||
raidsDate.setHours(startHour, 0, 0, 0);
|
||||
raidsDate.setDate(raidsDate.getDate() + 4);
|
||||
this._raidStages[2].endTime = raidsDate.getTime();
|
||||
this._matStepperIntl.optionalLabel = this._raidStages[1].duration.toString() + 'h';
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.guildMembers.map((data) => {
|
||||
data.raids = [];
|
||||
data.raidsInfo = {
|
||||
variationAvg: 0,
|
||||
variationSum: 0
|
||||
};
|
||||
return data;
|
||||
});
|
||||
|
||||
for (const member of Object.entries(guildRaids)) {
|
||||
const index = this.guildMembers.findIndex(item => item.id === member[0]);
|
||||
const memberRaids: HWGuildRaid[] = [];
|
||||
if (index !== -1) {
|
||||
for (const data of Object.entries(member[1])) {
|
||||
const raid: HWGuildRaid = {} as HWGuildRaid;
|
||||
Object.assign(raid, data[1]);
|
||||
const startTime: number = (parseInt(raid.startTime) * 1000);
|
||||
if (this._guildRaids.length === 0) {
|
||||
this._setStagesTime(startTime);
|
||||
}
|
||||
const raidsDate = new Date(startTime);
|
||||
if (raidsDate.getTime() > this._raidStages[1].endTime) {
|
||||
raid.stage = this._raidStages[2];
|
||||
} else if (raidsDate.getTime() > this._raidStages[0].endTime) {
|
||||
raid.stage = this._raidStages[1];
|
||||
} else {
|
||||
raid.stage = this._raidStages[0];
|
||||
}
|
||||
|
||||
raid.damage = data[1].result.damage;
|
||||
raid.level = data[1].result.level;
|
||||
raid.userName = this.guildMembers[index].name;
|
||||
|
||||
const attackers: HWGuildRaidAttackers = {} as HWGuildRaidAttackers;
|
||||
Object.assign(attackers, data[1].attackers);
|
||||
//Object.assign(raid.attackers, data[1].attackers);
|
||||
raid.attackers = [];
|
||||
raid.power = 0;
|
||||
for (const item of Object.entries(attackers)) {
|
||||
const attacker: HWGuildRaidAttacker = {
|
||||
id: item[1].id,
|
||||
power: item[1].power,
|
||||
type: item[1].type
|
||||
} as HWGuildRaidAttacker;
|
||||
raid.power += attacker.power;
|
||||
raid.attackers.push(attacker);
|
||||
}
|
||||
let percent = 0;
|
||||
if (raid.stage.maxPower > 0) {
|
||||
percent = (((raid.power / raid.stage.maxPower) - 1) * 100);
|
||||
}
|
||||
let color = 'default';
|
||||
let value = 0;
|
||||
if (raid.stage.maxPower > 0) {
|
||||
if (percent > 15) {
|
||||
color = 'raspberry';
|
||||
} else if (percent > 10) {
|
||||
color = 'red';
|
||||
} else if (percent > 5) {
|
||||
color = 'orange';
|
||||
}
|
||||
value = (raid.power - raid.stage.maxPower)
|
||||
}
|
||||
raid.variation = {
|
||||
value: value,
|
||||
percent: percent,
|
||||
color: color
|
||||
};
|
||||
raid.tooltip = Math.floor(raid.variation.value / 1000) + 'k';
|
||||
//console.log(raid.attackers);
|
||||
|
||||
memberRaids.push(raid);
|
||||
this._guildRaids.push(raid);
|
||||
}
|
||||
this.guildMembers[index].raids = memberRaids;
|
||||
const varSum = memberRaids.reduce((accumulator, currentValue) => accumulator + currentValue.variation.percent, 0);
|
||||
this.guildMembers[index].raidsInfo = {
|
||||
variationAvg: (varSum / memberRaids.length),
|
||||
variationSum: varSum
|
||||
};
|
||||
}
|
||||
}
|
||||
this._guildRaids.sort((a, b) => (a.startTime < b.startTime ? -1 : 1)).map((data) => {
|
||||
return data;
|
||||
});
|
||||
this.guildMembers.sort((a, b) => (a.raidsInfo.variationAvg > b.raidsInfo.variationAvg ? -1 : 1));/*.map((data) => {
|
||||
console.log(data.name, data.raidsInfo.variationAvg, data.raids.length);
|
||||
return data;
|
||||
});*/
|
||||
}
|
||||
|
||||
public getMembers(): HWMember[] {
|
||||
//return this.guildMembers;
|
||||
return this.minVarPct ? this.guildMembers.filter(member => (member.raidsInfo.variationAvg >= this.minVarPct) === true) : this.guildMembers;
|
||||
}
|
||||
|
||||
public getRaidsLog(stageNum: number): HWGuildRaid[] {
|
||||
return this._guildRaids.filter(raid => (raid.stage.num === stageNum) === true);
|
||||
}
|
||||
|
||||
public getStagesTime(stageNum: number): HWGuildRaidStage {
|
||||
return this._raidStages[stageNum];
|
||||
}
|
||||
|
||||
public onDateChange(): void {
|
||||
const timestamp: number = Date.parse(this.raidsForm.controls['startTime'].value);
|
||||
if (isNaN(timestamp) == false) {
|
||||
this._setStagesTime(timestamp);
|
||||
}
|
||||
}
|
||||
|
||||
submitForm(): void {
|
||||
}
|
||||
|
||||
}
|
||||
+149
@@ -0,0 +1,149 @@
|
||||
<mat-card class="my-3">
|
||||
<mat-card-header class="text-bg-warn d-flex rounded-top py-2 px-3">
|
||||
<div class="flex-fill">
|
||||
<mat-card-title>
|
||||
Guild War Day {{ warInfo.day }} VS {{ warInfo.clanEnemyName }}
|
||||
</mat-card-title>
|
||||
</div>
|
||||
<span class="flex-spacer"></span>
|
||||
<div class="align-self-center text-end">
|
||||
<span class="fs-5 font-light"> {{ warInfo.endTime | date: 'dd/MM/yyyy HH:mm:ss' }}</span>
|
||||
</div>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
<mat-divider class="mb-3"></mat-divider>
|
||||
<div class="d-flex align-items-center ps-2 mb-3">
|
||||
<dl class="dl-horizontal dl-large mt-3 mb-0">
|
||||
<dt>Server End Time</dt>
|
||||
<dd class="font-monospace">{{ warInfo.endTime | date: 'dd/MM/yyyy HH:mm:ss' }}</dd>
|
||||
<dt>Next War Time</dt>
|
||||
<dd class="font-monospace">{{ warInfo.nextWarTime| date: 'dd/MM/yyyy HH:mm:ss' }}</dd>
|
||||
<dt>Next Lock Time</dt>
|
||||
<dd class="font-monospace">{{ warInfo.nextLockTime | date: 'dd/MM/yyyy HH:mm:ss' }}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
<mat-divider class="my-3"></mat-divider>
|
||||
<mat-card class="my-3">
|
||||
<mat-card-header class="text-bg-danger d-flex rounded-top py-2 px-3">
|
||||
<div class="flex-fill">
|
||||
<mat-card-title>
|
||||
{{ warInfo.clanEnemyName }} Members
|
||||
</mat-card-title>
|
||||
</div>
|
||||
<span class="flex-spacer"></span>
|
||||
<div class="align-self-center text-end">
|
||||
<span class="fs-5 font-light">{{ subtitle }} {{ warInfo.endTime | date: 'dd/MM/yyyy HH:mm:ss' }}</span>
|
||||
</div>
|
||||
</mat-card-header>
|
||||
<mat-card-content class="p-0">
|
||||
<table class="table table-striped table-condensed border-top border-light">
|
||||
<thead>
|
||||
<tr role="row" class="text-bg-danger bg-opacity-50">
|
||||
<th class="align-middle w-auto ps-4">
|
||||
<div class="text-start">
|
||||
#
|
||||
</div>
|
||||
</th>
|
||||
<th scope="col" class="align-middle w-auto">
|
||||
<div class="d-flex align-items-center text-start">
|
||||
Name
|
||||
<mat-icon fontIcon="arrow_upward" class="ms-1 icon-xsmall"></mat-icon>
|
||||
</div>
|
||||
</th>
|
||||
<th scope="col" class="align-middle w-auto">
|
||||
<div class="text-start">
|
||||
Last Login
|
||||
</div>
|
||||
</th>
|
||||
<th class="align-middle w-auto">
|
||||
<div class="text-end">
|
||||
Heroes
|
||||
</div>
|
||||
</th>
|
||||
<th class="align-middle w-auto pe-4">
|
||||
<div class="text-end">
|
||||
Titans
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (enemy of getEnemies(); track enemy; let index = $index) {
|
||||
<tr role="row" class="align-middle{{ enemy.champion ? '' : ' text-muted' }}">
|
||||
<td class="cell text-start ps-4">
|
||||
{{ (index+1) }}
|
||||
</td>
|
||||
<td class="cell text-start">
|
||||
<div class="d-flex{{ enemy.champion ? ' text-green' : '' }}">
|
||||
<mat-icon [fontIcon]="enemy.champion ? 'gpp_good' : 'gpp_bad'" class="me-2 icon-small"></mat-icon>
|
||||
{{ enemy.name }}
|
||||
</div>
|
||||
</td>
|
||||
<td class="cell font-monospace text-start">
|
||||
<span class="me-3">{{ enemy.lastLoginTime + '000' | date: 'dd/MM HH:mm' }}</span>
|
||||
</td>
|
||||
<td class="cell font-monospace text-end">
|
||||
{{ enemy.heroes.power | number : '1.0' }}
|
||||
</td>
|
||||
<td class="cell font-monospace text-end">
|
||||
{{ enemy.titans.power | number : '1.0' }}
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
<mat-divider class="my-3"></mat-divider>
|
||||
<div class="row g-0">
|
||||
<div class="col col-md-6 pe-md-2">
|
||||
<mat-card>
|
||||
<mat-card-header class="bg-accent text-navy d-flex rounded-top py-2 px-3">
|
||||
<div class="flex-fill">
|
||||
<mat-card-title>
|
||||
{{ warInfo.clanEnemyName }} Heroes {{ title }}
|
||||
</mat-card-title>
|
||||
</div>
|
||||
<span class="flex-spacer"></span>
|
||||
<div class="align-self-center text-end">
|
||||
<span class="fs-5 font-light">{{ subtitle }}</span>
|
||||
</div>
|
||||
</mat-card-header>
|
||||
<mat-card-content class="pt-3">
|
||||
<app-fortification-card-content type="heroes" [fortification]="getLighthouse()" [members]="guildEnemies"></app-fortification-card-content>
|
||||
<app-fortification-card-content type="heroes" [fortification]="getBarracks()" [members]="guildEnemies"></app-fortification-card-content>
|
||||
<app-fortification-card-content type="heroes" [fortification]="getMageAcademy()" [members]="guildEnemies"></app-fortification-card-content>
|
||||
<mat-divider class="my-3"></mat-divider>
|
||||
<app-fortification-card-content type="heroes" [fortification]="getFoundry()" [members]="guildEnemies"></app-fortification-card-content>
|
||||
<mat-divider class="my-3"></mat-divider>
|
||||
<app-fortification-card-content type="heroes" [fortification]="getCitadel()" [members]="guildEnemies"></app-fortification-card-content>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</div>
|
||||
<div class="col col-md-6 ps-md-2">
|
||||
<mat-card>
|
||||
<mat-card-header class="bg-accent text-navy d-flex rounded-top py-2 px-3">
|
||||
<div class="flex-fill">
|
||||
<mat-card-title>
|
||||
{{ warInfo.clanEnemyName }} Titans {{ title }}
|
||||
</mat-card-title>
|
||||
</div>
|
||||
<span class="flex-spacer"></span>
|
||||
<div class="align-self-center text-end">
|
||||
<span class="fs-5 font-light">{{ subtitle }}</span>
|
||||
</div>
|
||||
</mat-card-header>
|
||||
<mat-card-content class="pt-3">
|
||||
<app-fortification-card-content type="titans" [fortification]="getBridge()" [members]="guildEnemies"></app-fortification-card-content>
|
||||
<mat-divider class="my-3"></mat-divider>
|
||||
<app-fortification-card-content type="titans" [fortification]="getGatesOfNature()" [members]="guildEnemies"></app-fortification-card-content>
|
||||
<app-fortification-card-content type="titans" [fortification]="getBastionOfFire()" [members]="guildEnemies"></app-fortification-card-content>
|
||||
<app-fortification-card-content type="titans" [fortification]="getBastionOfIce()" [members]="guildEnemies"></app-fortification-card-content>
|
||||
<mat-divider class="my-3"></mat-divider>
|
||||
<app-fortification-card-content type="titans" [fortification]="getSpringOfElements()" [members]="guildEnemies"></app-fortification-card-content>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</div>
|
||||
</div>
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { GuildwarAttackComponent } from './guildwar-attack.component';
|
||||
|
||||
describe('GuildwarAttackComponent', () => {
|
||||
let component: GuildwarAttackComponent;
|
||||
let fixture: ComponentFixture<GuildwarAttackComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [GuildwarAttackComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(GuildwarAttackComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
+285
@@ -0,0 +1,285 @@
|
||||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { DatePipe, DecimalPipe } from '@angular/common';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MatDividerModule } from '@angular/material/divider';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
|
||||
import { FortificationCardContentComponent } from '@components/shared';
|
||||
import {
|
||||
HWGuildWarEnemySlot, HWGuildWarEnemyTeam, HWGuildWarFortification,
|
||||
HWGuildWarHeroTeam, HWGuildWarTitanTeam, HWGuildWarSlots, HWMember
|
||||
} from '@models';
|
||||
|
||||
/* JSON data */
|
||||
import guildWarData from 'src/files-data/hw-guild-war.json'; // page Overview -> Statistics | page Guild War -> Guild War
|
||||
import guildWarInfo from 'src/files-data/hw-guild-war-info.json'; // page Guild War -> Guild War
|
||||
//import guildWarLog from 'src/files-data/hw-guild-war-log.json'; // page Guild War -> Guild War
|
||||
import guildWarSlots from 'src/files-data/hw-guild-war-slots.json'; // no pages
|
||||
|
||||
@Component({
|
||||
selector: 'app-guildwar-attack',
|
||||
standalone: true,
|
||||
imports: [
|
||||
DatePipe, DecimalPipe,
|
||||
MatCardModule, MatDividerModule, MatIconModule,
|
||||
FortificationCardContentComponent
|
||||
],
|
||||
templateUrl: './guildwar-attack.component.html',
|
||||
styleUrl: './guildwar-attack.component.scss'
|
||||
})
|
||||
export class GuildwarAttackComponent implements OnInit {
|
||||
private _championsByPower: HWMember[] = [];
|
||||
private _enemySlots: number[] = [];
|
||||
public guildEnemies: HWMember[] = [];
|
||||
public guildMembers: HWMember[] = [];
|
||||
public title = 'Fortifications';
|
||||
public subtitle = 'Guild War Attack';
|
||||
public now = new Date();
|
||||
public warInfo: { day: number, clanEnemyName: string, endTime: number, nextWarTime: number, nextLockTime: number } = {
|
||||
day: 0,
|
||||
clanEnemyName: '',
|
||||
endTime: 0,
|
||||
nextWarTime: 0,
|
||||
nextLockTime: 0
|
||||
};
|
||||
|
||||
@Input() set members(value: HWMember[]) {
|
||||
this.guildMembers = value;
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.warInfo.day = parseInt(guildWarInfo.day);
|
||||
this.warInfo.clanEnemyName = guildWarInfo.enemyClan.title;
|
||||
this.warInfo.endTime = (guildWarInfo.endTime * 1000);
|
||||
this.warInfo.nextWarTime = (guildWarInfo.nextWarTime * 1000);
|
||||
this.warInfo.nextLockTime = (guildWarInfo.nextLockTime * 1000);
|
||||
this._setChampionsByPower();
|
||||
this._setEnemies();
|
||||
}
|
||||
|
||||
private _getFortification(indexes: number[], type: string, name: string): HWGuildWarFortification {
|
||||
const teams: HWMember[] = [];
|
||||
const slots: HWGuildWarSlots = guildWarSlots.slots;
|
||||
let count = 0;
|
||||
indexes.forEach((data) => {
|
||||
switch (type) {
|
||||
case 'heroes':
|
||||
teams.push(this.guildEnemies[data]);
|
||||
count += this.guildEnemies[data].heroes.power;
|
||||
break;
|
||||
case 'titans':
|
||||
teams.push(this.guildEnemies[data]);
|
||||
count += this.guildEnemies[data].titans.power;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
return {
|
||||
name: name,
|
||||
type: type,
|
||||
count: count,
|
||||
teams: teams,
|
||||
positions: this._enemySlots,
|
||||
slots: slots[name]
|
||||
};
|
||||
}
|
||||
|
||||
private _getDefense(type: string, name: string): HWGuildWarFortification {
|
||||
const teams: HWMember[] = [];
|
||||
const slots: HWGuildWarSlots = guildWarSlots.slots;
|
||||
let count = 0;
|
||||
slots[name].forEach(slot => {
|
||||
let enemies: HWMember[] = [];
|
||||
let team: HWMember = {} as HWMember;
|
||||
enemies = this.guildEnemies.filter(enemy => parseInt(enemy.id) === this._enemySlots[slot]);
|
||||
if (enemies.length) {
|
||||
team = enemies[0];
|
||||
teams.push(team);
|
||||
switch (type) {
|
||||
case 'heroes':
|
||||
count += team.heroes.power;
|
||||
break;
|
||||
case 'titans':
|
||||
count += team.titans.power;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
name: name,
|
||||
type: type,
|
||||
count: count,
|
||||
teams: teams,
|
||||
positions: this._enemySlots,
|
||||
slots: slots[name]
|
||||
};
|
||||
}
|
||||
|
||||
private _setEnemies(): void {
|
||||
const warriors: number[] = [];
|
||||
for (const data of Object.entries(guildWarInfo.enemyClanTries)) {
|
||||
warriors.push(parseInt(data[0]));
|
||||
}
|
||||
for (const data of Object.entries(guildWarInfo.enemyClanMembers)) {
|
||||
const enemy: HWMember = {} as HWMember;
|
||||
Object.assign(enemy, data[1]);
|
||||
if (warriors.indexOf(parseInt(enemy.id)) !== -1) {
|
||||
enemy.champion = true;
|
||||
} else {
|
||||
enemy.champion = false;
|
||||
}
|
||||
enemy.heroes = { power: 0, teams: []};
|
||||
enemy.titans = { power: 0, teams: []};
|
||||
|
||||
//console.log(guildWarInfo.enemySlots);
|
||||
|
||||
this.guildEnemies.push(enemy);
|
||||
}
|
||||
|
||||
for (const data of Object.entries(guildWarInfo.enemySlots)) {
|
||||
const ennemySlot: HWGuildWarEnemySlot = {} as HWGuildWarEnemySlot;
|
||||
Object.assign(ennemySlot, data[1]);
|
||||
//console.log(ennemySlot.slotId, ennemySlot.user.name, ennemySlot.user.id); //, ennemySlot.team["1"].power);
|
||||
ennemySlot.teams = [];
|
||||
let totalHeroPower = 0;
|
||||
let totalTitanPower = 0;
|
||||
if (ennemySlot.user !== null) {
|
||||
this._enemySlots[ennemySlot.slotId] = parseInt(ennemySlot.user.id);
|
||||
for (const team of Object.entries(data[1].team[0])) {
|
||||
/*
|
||||
ennemySlot.teams.push(enemyTeam);
|
||||
console.log(enemyTeam);
|
||||
*/
|
||||
const enemyTeam: HWGuildWarEnemyTeam = {} as HWGuildWarEnemyTeam;
|
||||
Object.assign(enemyTeam, team[1]);
|
||||
switch (enemyTeam.type) {
|
||||
case 'hero':
|
||||
case 'pet':
|
||||
totalHeroPower += enemyTeam.power;
|
||||
break;
|
||||
case 'titan':
|
||||
totalTitanPower += enemyTeam.power;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
ennemySlot.teams.push(team[1]);
|
||||
//console.log(team[1]);
|
||||
}
|
||||
//console.log(totalHeroPower, totalTitanPower, ennemySlot.teams);
|
||||
|
||||
this.guildEnemies.some((item) => {
|
||||
if (item.id === ennemySlot.user.id) {
|
||||
if (totalHeroPower) {
|
||||
item.heroes.power = totalHeroPower;
|
||||
item.heroes.teams = ennemySlot.teams;
|
||||
}
|
||||
if (totalTitanPower) {
|
||||
item.titans.power = totalTitanPower;
|
||||
item.titans.teams = ennemySlot.teams;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
this.guildEnemies.sort((a, b) => ((a.heroes.power + a.titans.power) > (b.heroes.power + b.titans.power) ? -1 : 1)); // Descending
|
||||
}
|
||||
}
|
||||
|
||||
private _setChampionsByPower(): void {
|
||||
for (const [teamId, teamValues] of Object.entries(guildWarData.teams)) {
|
||||
const index = this.guildMembers.findIndex(member => member.id === teamId);
|
||||
let totalHeroPower = 0;
|
||||
let totalTitanPower = 0;
|
||||
const heroes: HWGuildWarHeroTeam[] = [];
|
||||
const titans: HWGuildWarTitanTeam[] = [];
|
||||
|
||||
for (const unit of Object.entries(teamValues.clanDefence_heroes.units)) {
|
||||
heroes.push(unit[1]);
|
||||
totalHeroPower += unit[1].power;
|
||||
}
|
||||
for (const unit of Object.entries(teamValues.clanDefence_titans.units)) {
|
||||
titans.push(unit[1]);
|
||||
totalTitanPower += unit[1].power;
|
||||
}
|
||||
this.guildMembers[index].heroes = { power: totalHeroPower, teams: heroes };
|
||||
this.guildMembers[index].titans = { power: totalTitanPower, teams: titans };
|
||||
}
|
||||
this.guildMembers.sort((a, b) => ((a.heroes.power + a.titans.power) > (b.heroes.power + b.titans.power) ? -1 : 1)); // Descending
|
||||
this._championsByPower = this.guildMembers.filter(member => member.champion === true);
|
||||
}
|
||||
|
||||
getEnemies(): HWMember[] {
|
||||
return this.guildEnemies;
|
||||
}
|
||||
|
||||
getMembers(): HWMember[] {
|
||||
return this.guildMembers;
|
||||
}
|
||||
|
||||
getLighthouse(): HWGuildWarFortification {
|
||||
//const indexes: number[] = [0, 4, 8];
|
||||
//return this._getFortification(indexes, 'heroes', 'Lighthouse');
|
||||
return this._getDefense('heroes', 'Lighthouse');
|
||||
}
|
||||
|
||||
getBarracks(): HWGuildWarFortification {
|
||||
//const indexes: number[] = [1, 5, 6];
|
||||
//return this._getFortification(indexes, 'heroes', 'Barracks');
|
||||
return this._getDefense('heroes', 'Barracks');
|
||||
}
|
||||
|
||||
getMageAcademy(): HWGuildWarFortification {
|
||||
//const indexes: number[] = [2, 3, 7];
|
||||
//return this._getFortification(indexes, 'heroes', 'Mage Academy');
|
||||
return this._getDefense('heroes', 'Mage Academy');
|
||||
}
|
||||
|
||||
getFoundry(): HWGuildWarFortification {
|
||||
//const indexes: number[] = [9, 11, 12, 13];
|
||||
//return this._getFortification(indexes, 'heroes', 'Foundry');
|
||||
return this._getDefense('heroes', 'Foundry');
|
||||
}
|
||||
|
||||
getCitadel(): HWGuildWarFortification {
|
||||
//const indexes: number[] = [10, 14, 15, 16, 17, 18, 19];
|
||||
//return this._getFortification(indexes, 'heroes', 'Citadel');
|
||||
return this._getDefense('heroes', 'Citadel');
|
||||
}
|
||||
|
||||
getBridge(): HWGuildWarFortification {
|
||||
//const indexes: number[] = [0, 1, 2, 3];
|
||||
//return this._getFortification(indexes, 'titans', 'Bridge');
|
||||
return this._getDefense('titans', 'Bridge');
|
||||
}
|
||||
|
||||
getGatesOfNature(): HWGuildWarFortification {
|
||||
//const indexes: number[] = [4, 9, 14, 19];
|
||||
//return this._getFortification(indexes, 'titans', 'Gates Of Nature');
|
||||
return this._getDefense('titans', 'Gates Of Nature');
|
||||
}
|
||||
|
||||
getBastionOfFire(): HWGuildWarFortification {
|
||||
//const indexes: number[] = [5, 10, 15, 16];
|
||||
//return this._getFortification(indexes, 'titans', 'Bastion Of Fire');
|
||||
return this._getDefense('titans', 'Bastion Of Fire');
|
||||
}
|
||||
|
||||
getBastionOfIce(): HWGuildWarFortification {
|
||||
//const indexes: number[] = [6, 11, 12, 17];
|
||||
//return this._getFortification(indexes, 'titans', 'Bastion Of Ice');
|
||||
return this._getDefense('titans', 'Bastion Of Ice');
|
||||
}
|
||||
|
||||
getSpringOfElements(): HWGuildWarFortification {
|
||||
//const indexes: number[] = [7, 8, 13, 18];
|
||||
//return this._getFortification(indexes, 'titans', 'Spring Of Elements');
|
||||
return this._getDefense('titans', 'Spring Of Elements');
|
||||
}
|
||||
|
||||
}
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
<mat-card class="my-3">
|
||||
<mat-card-header class="bg-{{color}}-light text-bg-primary d-flex rounded-top py-2 px-3">
|
||||
<div class="flex-fill">
|
||||
<mat-card-title>
|
||||
{{ title }}
|
||||
</mat-card-title>
|
||||
</div>
|
||||
<span class="flex-spacer"></span>
|
||||
<div class="align-self-center text-end">
|
||||
<span class="fs-5 font-light">{{ subtitle }}</span>
|
||||
</div>
|
||||
</mat-card-header>
|
||||
<mat-card-content class="p-0">
|
||||
<table class="table table-striped table-condensed border-top border-light">
|
||||
<thead>
|
||||
<tr role="row" class="bg-{{color}}-light text-bg-primary bg-opacity-50">
|
||||
<th class="align-middle w-auto ps-4">
|
||||
<div class="text-start">
|
||||
#
|
||||
</div>
|
||||
</th>
|
||||
<th class="align-middle w-auto">
|
||||
<div class="text-start">
|
||||
Name
|
||||
</div>
|
||||
</th>
|
||||
<th class="align-middle w-auto pe-4">
|
||||
<div class="d-flex align-items-center justify-content-end">
|
||||
Power
|
||||
<mat-icon fontIcon="arrow_downward" class="ms-1 icon-xsmall"></mat-icon>
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (champion of getChampionsByPower(); track champion; let index = $index) {
|
||||
<tr role="row" class="align-middle">
|
||||
<td class="cell text-start ps-4">
|
||||
{{ (index+1) }}
|
||||
</td>
|
||||
<td class="cell text-start">
|
||||
{{ champion.name }}
|
||||
</td>
|
||||
<td class="cell font-monospace text-end pe-4">
|
||||
@if (teamType === 'heroes') {
|
||||
{{ champion.heroes.power | number : '1.0' }}
|
||||
}
|
||||
@else {
|
||||
{{ champion.titans.power | number : '1.0' }}
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
+1
@@ -0,0 +1 @@
|
||||
/* guildwar-champions */
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { GuildwarChampionsComponent } from './guildwar-champions.component';
|
||||
|
||||
describe('GuildwarChampionsComponent', () => {
|
||||
let component: GuildwarChampionsComponent;
|
||||
let fixture: ComponentFixture<GuildwarChampionsComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [GuildwarChampionsComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(GuildwarChampionsComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { DecimalPipe } from '@angular/common';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MatDividerModule } from '@angular/material/divider';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
|
||||
import { HWMember } from '@models';
|
||||
//import { HWGuildWarFortification, HWMember } from '@models';
|
||||
|
||||
@Component({
|
||||
selector: 'app-guildwar-champions',
|
||||
standalone: true,
|
||||
imports: [
|
||||
DecimalPipe,
|
||||
MatCardModule, MatDividerModule, MatIconModule
|
||||
],
|
||||
templateUrl: './guildwar-champions.component.html',
|
||||
styleUrl: './guildwar-champions.component.scss'
|
||||
})
|
||||
export class GuildwarChampionsComponent implements OnInit {
|
||||
private _championsByPower: HWMember[] = [];
|
||||
public guildMembers: HWMember[] = [];
|
||||
public color: string = 'primary';
|
||||
public teamType: string = '';
|
||||
public title = 'Champions';
|
||||
public subtitle = 'by power';
|
||||
|
||||
@Input() set members(value: HWMember[]) {
|
||||
this.guildMembers = value;
|
||||
}
|
||||
|
||||
@Input() set type(value: string) {
|
||||
this.teamType = value;
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
switch (this.teamType) {
|
||||
case 'heroes':
|
||||
this.color = 'navy';
|
||||
this.subtitle = 'heroes power';
|
||||
this.guildMembers.sort((a, b) => (a.heroes.power > b.heroes.power ? -1 : 1)); // Descending
|
||||
this._championsByPower = this.guildMembers.filter(member => member.champion === true);
|
||||
break;
|
||||
case 'titans':
|
||||
this.color = 'purple';
|
||||
this.subtitle = 'titans power';
|
||||
this.guildMembers.sort((a, b) => (a.titans.power > b.titans.power ? -1 : 1)); // Descending
|
||||
this._championsByPower = this.guildMembers.filter(member => member.champion === true);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
getChampionsByPower(): HWMember[] {
|
||||
return this._championsByPower;
|
||||
}
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
<mat-card class="my-3">
|
||||
<mat-card-header class="bg-accent text-navy d-flex rounded-top py-2 px-3">
|
||||
<div class="flex-fill">
|
||||
<mat-card-title>
|
||||
{{ title }}
|
||||
</mat-card-title>
|
||||
</div>
|
||||
<span class="flex-spacer"></span>
|
||||
<div class="align-self-center text-end">
|
||||
<span class="fs-5 font-light">{{ subtitle }}</span>
|
||||
</div>
|
||||
</mat-card-header>
|
||||
<mat-card-content class="pt-3">
|
||||
@if (teamType === 'heroes') {
|
||||
<app-fortification-card-content [type]="teamType" [fortification]="getLighthouse()" [members]="guildMembers"></app-fortification-card-content>
|
||||
<app-fortification-card-content [type]="teamType" [fortification]="getBarracks()" [members]="guildMembers"></app-fortification-card-content>
|
||||
<app-fortification-card-content [type]="teamType" [fortification]="getMageAcademy()" [members]="guildMembers"></app-fortification-card-content>
|
||||
<mat-divider class="my-3"></mat-divider>
|
||||
<app-fortification-card-content [type]="teamType" [fortification]="getFoundry()" [members]="guildMembers"></app-fortification-card-content>
|
||||
<mat-divider class="my-3"></mat-divider>
|
||||
<app-fortification-card-content [type]="teamType" [fortification]="getCitadel()" [members]="guildMembers"></app-fortification-card-content>
|
||||
}
|
||||
@else {
|
||||
<app-fortification-card-content [type]="teamType" [fortification]="getBridge()" [members]="guildMembers"></app-fortification-card-content>
|
||||
<mat-divider class="my-3"></mat-divider>
|
||||
<app-fortification-card-content [type]="teamType" [fortification]="getGatesOfNature()" [members]="guildMembers"></app-fortification-card-content>
|
||||
<app-fortification-card-content [type]="teamType" [fortification]="getBastionOfFire()" [members]="guildMembers"></app-fortification-card-content>
|
||||
<app-fortification-card-content [type]="teamType" [fortification]="getBastionOfIce()" [members]="guildMembers"></app-fortification-card-content>
|
||||
<mat-divider class="my-3"></mat-divider>
|
||||
<app-fortification-card-content [type]="teamType" [fortification]="getSpringOfElements()" [members]="guildMembers"></app-fortification-card-content>
|
||||
}
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
<div class="d-flex align-items-center ps-2 mb-3">
|
||||
<mat-icon fontIcon="fiber_manual_record" class="me-2 icon-small text-red"></mat-icon> <span class="d-inline-block">Position to check</span>
|
||||
</div>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user