Import des sources angular à partir de 'headup_app'

This commit is contained in:
Rampeur
2025-08-11 23:26:29 +02:00
parent 7dcb426ef5
commit 0a6cbc0c00
335 changed files with 64362 additions and 0 deletions
@@ -0,0 +1,29 @@
<table class="table table-striped table-dark table-hover">
<thead>
<tr>
<th></th>
<th *ngFor='let name of headers; let i=index' class="text-end"> {{name}} </th>
<th class="text-end">Total</th>
</tr>
</thead>
<tbody>
<tr *ngFor='let row of rows; let i=index'>
<th class="{{colors[i]}} text-end"> ● {{ names[i] }} </th>
<td *ngFor='let value of row; let i=index' class="font-monospace text-end">
<span class="{{ value === 0 ? 'text-empty pe-1' : 'pe-1'}}">{{ value }}</span>
</td>
<th class="{{colors[i]}} font-monospace text-end">
<span class="{{ values[i] === 0 ? 'text-empty pe-1' : 'pe-1'}}">{{ values[i] }}</span>
</th>
</tr>
</tbody>
<tfoot>
<tr>
<th class="text-end">Total</th>
<th *ngFor='let value of seriesColTotal; let i=index' class="font-monospace text-end">
<span class="{{ value === 0 ? 'text-empty pe-1' : 'pe-1'}}">{{ value }}</span>
</th>
<th class="font-monospace text-end">{{ grandTotal }}</th>
</tr>
</tfoot>
</table>
@@ -0,0 +1,24 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { HistoryTableComponent } from './history-table.component';
describe('HistoryTableComponent', () => {
let component: HistoryTableComponent;
let fixture: ComponentFixture<HistoryTableComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [HistoryTableComponent]
})
.compileComponents();
fixture = TestBed.createComponent(HistoryTableComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,46 @@
import { Component, Input, AfterContentChecked } from '@angular/core';
import { NgIf, NgFor } from '@angular/common';
@Component({
standalone: true,
imports: [
NgIf, NgFor
],
selector: 'app-history-table',
templateUrl: './history-table.component.html',
styleUrls: []
})
export class HistoryTableComponent implements AfterContentChecked {
public seriesColTotal: number[] = [];
public grandTotal: number = 0;
constructor() {}
@Input() headers: string[] = [];
@Input() names: string[] = [];
@Input() values: number[] = [];
@Input() rows: Array<Array<number>> = [];
@Input() colors: string[] = [];
/*ngOnChanges() {
//this._loadHistoryTable();
//console.log('onChanges');
}*/
ngAfterContentChecked() {
if (this.rows !== undefined && this.rows.length) {
this._loadHistoryTable();
}
}
private _loadHistoryTable(): void {
const values: Array<number> = this.rows[0].map(() => 0);
this.seriesColTotal = [...values];
this.rows.forEach((row: number[], rowIndex: number) => {
row.forEach((value: number, index: number) => {
this.seriesColTotal[index] = (this.seriesColTotal[index] + this.rows[rowIndex][index]);
});
this.grandTotal = this.seriesColTotal.reduce((partialSum, a) => partialSum + a, 0);
});
}
}
@@ -0,0 +1,5 @@
export * from './history-table.component';
export * from './jump-list.component';
export * from './jump-meta.component';
export * from './jump-preview.component';
export * from './jump-table.component';
@@ -0,0 +1,22 @@
<div class="clearfix mt-4 mb-1 border-bottom">
<h1 class="float-md-start me-3">{{ title }} <span class="fs-4 text-accent" [hidden]="!jumpsCount">({{ jumpsCount }})</span></h1>
<mat-spinner class="float-md-start" [diameter]="32" [hidden]="!loading" color="accent"></mat-spinner>
<button [hidden]="!showAdd" mat-raised-button color="accent" [routerLink]="['/editor']" class="float-md-end">
<mat-icon aria-label="Ajouter" fontIcon="add_notes"></mat-icon> Ajouter
</button>
</div>
<span [hidden]="!jumps.length" class="small mt-2 me-2 fst-italic">Prix actualisés toutes les {{ (refresh / 1000) }} secondes.</span>
<span [hidden]="jumps.length || loading">Aucun produit à afficher pour le moment</span>
<span [hidden]="!loading">Chargement des produits en cours</span>
@for (jump of jumps; track jump) {
<app-jump-preview [jump]="jump" [isUser]="isUser" [showMeta]="showMeta"></app-jump-preview>
}
<nav [hidden]="loading || pages.length <= 1" class="clearfix mt-3">
<ul class="pagination float-end">
@for (pageNumber of pages; track pageNumber) {
<li class="page-item" [ngClass]="{'active': pageNumber === currentPage}">
<button class="page-link" (click)="setPageTo(pageNumber)">{{ pageNumber }}</button>
</li>
}
</ul>
</nav>
@@ -0,0 +1,96 @@
import { Component, OnInit, OnDestroy, Input } from '@angular/core';
import { NgClass, DatePipe } from '@angular/common';
import { RouterLink } from '@angular/router';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { Observable, Subscription } from 'rxjs';
import { Errors, Jump, JumpList, JumpListConfig } from 'src/app/core/models';
import { JumpsService } from 'src/app/core/services';
import { JumpPreviewComponent } from './jump-preview.component';
@Component({
selector: 'app-jump-list',
templateUrl: './jump-list.component.html',
standalone: true,
imports: [
NgClass,
DatePipe,
MatProgressSpinnerModule,
MatButtonModule,
RouterLink,
MatIconModule,
JumpPreviewComponent
]
})
export class JumpListComponent implements OnInit, OnDestroy {
private _jumps: Subscription = new Subscription();
errors: Errors = { errors: {} };
query!: JumpListConfig;
jumps: Array<Jump> = [];
jumpsCount = 0;
loading = false;
currentPage = 1;
pages: Array<number> = [1];
lastUpdate: Date = new Date();
constructor(
private jumpsService: JumpsService
) { }
@Input() limit = 0;
@Input() refresh = 30000;
@Input() title = '';
@Input() showAdd = false;
@Input() isUser = false;
@Input() showMeta = false;
@Input()
set config(config: JumpListConfig) {
if (config) {
this.query = config;
this.currentPage = 1;
this.loading = true;
}
}
ngOnInit() {
this.runQuery();
}
ngOnDestroy() {
this._jumps.unsubscribe();
}
setPageTo(pageNumber: number) {
this.currentPage = pageNumber;
this.runQuery();
}
runQuery() {
if (this.limit) {
this.query.filters.limit = this.limit;
this.query.filters.offset = (this.limit * (this.currentPage - 1));
}
const jumps$: Observable<JumpList> = this.jumpsService.query(this.query);
this._jumps = jumps$.subscribe({
next: (data: JumpList) => {
this.loading = false;
this.jumps = data.jumps;
this.jumpsCount = data.jumpsCount;
if (this.limit) {
this.pages = this.getTotalPages(this.jumpsCount, this.limit);
}
},
error: (err) => {
this.errors = err;
}
});
}
getTotalPages(count: number, limit: number) {
// Used from http://www.jstips.co/en/create-range-0...n-easily-using-one-line/
return Array.from(new Array(Math.ceil(count / limit)), (val, index) => index + 1);
}
}
@@ -0,0 +1,14 @@
<div class="jump-meta float-end">
<a [routerLink]="['/profile', jump.author.username]">
<img [src]="jump.author.image" [alt]="jump.author.username" />
</a>
<div class="info">
<a class="author" [routerLink]="['/profile', jump.author.username]">
{{ jump.author.username }}
</a>
<span class="date">
{{ jump.createdAt | date: 'longDate' }}
</span>
</div>
<ng-content></ng-content>
</div>
@@ -0,0 +1,29 @@
/* Jumps Meta */
.jump-meta {
display: block;
position: relative;
font-weight: 300;
img {
display: inline-block;
vertical-align: middle;
height: 32px;
width: 32px;
border-radius: 30px;
}
.info {
margin: 0 1rem 0 .4rem;
display: inline-block;
vertical-align: middle;
line-height: 1rem;
.author {
display: block;
font-weight: 500 !important;
margin-bottom: 0.1rem;
}
.date {
color: #bbb;
font-size: .8rem;
display: block;
}
}
}
@@ -0,0 +1,16 @@
import { Component, Input } from '@angular/core';
import { DatePipe } from '@angular/common';
import { RouterLink } from '@angular/router';
import { Jump } from 'src/app/core/models';
@Component({
selector: 'app-jump-meta',
standalone: true,
imports: [ RouterLink, DatePipe ],
styleUrl: './jump-meta.component.scss',
templateUrl: './jump-meta.component.html'
})
export class JumpMetaComponent {
@Input() jump!: Jump;
}
@@ -0,0 +1,44 @@
<mat-card appearance="outlined" class="mt-4">
<mat-card-header class="pt-2">
<mat-card-title-group>
<mat-card-title><a [routerLink]="['/jump', jump.slug]">#{{ jump.numero }}</a></mat-card-title>
<mat-card-subtitle>{{ jump.categorie }} / {{ jump.module }} </mat-card-subtitle>
</mat-card-title-group>
</mat-card-header>
<mat-card-content>
<mat-divider [class]="['mt-1', 'mb-2']"></mat-divider>
<app-jump-meta [hidden]="!showMeta" [jump]="jump" class="float-sm-end"></app-jump-meta>
<div class="row mt-4">
<div class="col-sm-6">
<dl class="row mb-2">
<dt class="col-lg-4 col-md-5 col-sm-6 fw-normal">Date</dt>
<dd class="col-lg-8 col-md-7 col-sm-6">{{ jump.date }}</dd>
<dt class="col-lg-4 col-md-5 col-sm-6 fw-normal">Lieu</dt>
<dd class="col-lg-8 col-md-7 col-sm-6">{{ jump.lieu }} {{ jump.oaci }}</dd>
<dt class="col-lg-4 col-md-5 col-sm-6 fw-normal">Voile</dt>
<dd class="col-lg-8 col-md-7 col-sm-6">{{ jump.voile }} {{ jump.taille }}</dd>
</dl>
</div>
<div class="col-sm-6">
<dl class="row mb-2">
<dt class="col-lg-4 col-md-5 col-sm-6 fw-normal">Hauteur</dt>
<dd class="col-lg-8 col-md-7 col-sm-6">{{ jump.hauteur }}</dd>
<dt class="col-lg-4 col-md-5 col-sm-6 fw-normal">Aeronef</dt>
<dd class="col-lg-8 col-md-7 col-sm-6">{{ jump.aeronef }} {{ jump.imat }}</dd>
<dt class="col-lg-4 col-md-5 col-sm-6 fw-normal">Type de saut</dt>
<dd class="col-lg-8 col-md-7 col-sm-6">{{ jump.categorie }} {{ jump.module }}</dd>
</dl>
</div>
<div class="col-xs-12">
<h5>Participants ({{ jump.participants }})</h5>
</div>
</div>
<mat-divider [class]="['mt-2']"></mat-divider>
</mat-card-content>
<mat-card-actions>
<button mat-button color="danger" [disabled]="isDeleting" (click)="deleteJump()">SUPPRIMER</button>
<span class="flex-spacer"></span>
<button mat-button [routerLink]="['/editor', jump.slug]">MODIFIER</button>
<button mat-button color="primary" [routerLink]="['/jump', jump.slug]">VOIR</button>
</mat-card-actions>
</mat-card>
@@ -0,0 +1,44 @@
/* Jumps preview */
.jump-preview {
border-top: 1px solid rgba(0, 0, 0, .1);
padding: 1.4rem 0 0.5rem;
.jump-meta {
margin: 0 0 0.2rem;
}
h3 .preview-link {
font-weight: 600 !important;
font-size: 1.5rem !important;
margin-bottom: 3px;
}
}
.preview-link {
color: inherit !important;
&:hover {
text-decoration: inherit !important;
}
p {
color: #999;
margin-bottom: 15px;
font-size: 1rem;
font-weight: 300;
line-height: 1.3rem;
}
span {
max-width: 30%;
font-size: .8rem;
font-weight: 300;
color: #bbb;
vertical-align: middle;
}
ul {
float: right;
max-width: 50%;
vertical-align: top;
}
ul li {
font-weight: 300;
font-size: .8rem !important;
padding-top: .2rem;
padding-bottom: .2rem;
}
}
@@ -0,0 +1,49 @@
import { Component, OnDestroy, Input } from '@angular/core';
import { DecimalPipe } from '@angular/common';
import { Router, RouterLink } from '@angular/router';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatDividerModule } from '@angular/material/divider';
import { Observable, Subscription } from 'rxjs';
import { JumpMetaComponent } from './jump-meta.component';
import { Jump } from 'src/app/core/models';
import { JumpsService } from 'src/app/core/services';
@Component({
selector: 'app-jump-preview',
standalone: true,
imports: [
DecimalPipe, RouterLink,
MatDividerModule, MatButtonModule, MatCardModule,
JumpMetaComponent
],
styleUrl: './jump-preview.component.scss',
templateUrl: './jump-preview.component.html'
})
export class JumpPreviewComponent implements OnDestroy {
@Input() jump!: Jump;
@Input() isUser!: boolean;
@Input() showMeta!: boolean;
private _jump: Subscription = new Subscription();
isDeleting = false;
constructor(
private router: Router,
private jumpsService: JumpsService
) { }
ngOnDestroy() {
this._jump.unsubscribe();
}
deleteJump() {
this.isDeleting = true;
const jump$: Observable<boolean> = this.jumpsService.destroy(this.jump.slug);
this._jump = jump$.subscribe({
next: () => {
this.router.navigateByUrl('/');
}
});
}
}
@@ -0,0 +1,314 @@
<div class="row mx-0">
<div class="col-md-6 col-xs-12 px-0">
<form class="mt-3" [formGroup]="searchForm" (ngSubmit)="submitForm()">
<mat-form-field appearance="fill">
<input matInput type="number" placeholder="Rechercher un saut" formControlName="numero">
@if (this.searchForm.controls['numero'].status === 'INVALID') {
<mat-error>{{getErrorMessage()}}</mat-error>
}
</mat-form-field>
<button mat-raised-button color="primary" type="submit" class="ms-2">
<mat-icon aria-label="Rechercher" fontIcon="search"></mat-icon> Rechercher
</button>
<mat-spinner [diameter]="32" [hidden]="!loading" color="accent" class="align-middle ms-2"></mat-spinner>
<span [hidden]="jumpsCount || loading" class="ms-2 fst-italic">Aucun saut à afficher pour le moment.</span>
<span [hidden]="!loading" class="ms-2 fst-italic">Chargement des sauts en cours</span>
</form>
</div>
<div class="col-md-6 col-xs-12 px-0">
<mat-paginator [pageSize]="25" [pageSizeOptions]="[10, 25, 50, 100]" showFirstLastButtons class="mb-1 mt-2">
</mat-paginator>
</div>
</div>
<mat-progress-bar [mode]="loading ? 'query' : 'determinate'" [value]="loading ? 0 : 100" color="navy-light">
</mat-progress-bar>
<table mat-table [dataSource]="jumps" matSort
class="table table-dark table-striped table-hover table-condensed mat-elevation-z2">
<caption [hidden]="jumpsCount || loading">Aucun saut à afficher pour le moment.</caption>
<!-- Numero Column -->
<ng-container matColumnDef="numero">
<th mat-header-cell *matHeaderCellDef mat-sort-header> # </th>
<td class="pl-3 pr-3" mat-cell *matCellDef="let element">
@if (element.numero) {
<!--<a [routerLink]="['/jump', element.slug]" class="accent">{{element.numero}}</a>-->
<button mat-button (click)="openViewDialog(element)" color="primary">{{element.numero}}</button>
}
@if (!element.numero) {
<ngx-skeleton-loader [theme]="{width: '30px', height: '13px', marginBottom: '0px'}">
</ngx-skeleton-loader>
}
</td>
</ng-container>
<!-- Date Column -->
<ng-container matColumnDef="date">
<th class="pl-3" mat-header-cell *matHeaderCellDef> Date </th>
<td class="pl-3 text-nowrap" mat-cell *matCellDef="let element">
@if (element.date) {
{{element.date | date: 'mediumDate'}}
}
@if (!element.numero) {
<ngx-skeleton-loader [theme]="{width: '30px', height: '13px', marginBottom: '0px'}">
</ngx-skeleton-loader>
}
</td>
</ng-container>
<!-- Lieu Column -->
<ng-container matColumnDef="lieu">
<th class="pl-3" mat-header-cell *matHeaderCellDef> Lieu </th>
<td class="pl-3 text-nowrap" mat-cell *matCellDef="let element">
@if (element.lieu) {
{{element.lieu}} - {{element.oaci}}
}
@if (!element.numero) {
<ngx-skeleton-loader [theme]="{width: '30px', height: '13px', marginBottom: '0px'}">
</ngx-skeleton-loader>
}
</td>
</ng-container>
<!-- Oaci Column
<ng-container matColumnDef="oaci">
<th class="pl-3" mat-header-cell *matHeaderCellDef> Oaci </th>
<td class="pl-3 text-nowrap" mat-cell *matCellDef="let element">
<ng-container *ngIf="element.oaci">{{element.oaci}}</ng-container>
<ng-container *ngIf="!element.numero">
<ngx-skeleton-loader [theme]="{width: '30px', height: '13px', marginBottom: '0px'}">
</ngx-skeleton-loader>
</ng-container>
</td>
</ng-container> -->
<!-- Aeronef Column -->
<ng-container matColumnDef="aeronef">
<th class="pl-3" mat-header-cell *matHeaderCellDef> Aeronef </th>
<td class="pl-3 text-nowrap" mat-cell *matCellDef="let element">
@if (element.aeronef) {
{{element.aeronef}} - {{element.imat}}
}
@if (!element.numero) {
<ngx-skeleton-loader [theme]="{width: '30px', height: '13px', marginBottom: '0px'}">
</ngx-skeleton-loader>
}
</td>
</ng-container>
<!-- Imat Column
<ng-container matColumnDef="imat">
<th class="pl-3" mat-header-cell *matHeaderCellDef> Imat </th>
<td class="pl-3 text-nowrap" mat-cell *matCellDef="let element">
<ng-container *ngIf="element.imat">{{element.imat}}</ng-container>
<ng-container *ngIf="!element.numero">
<ngx-skeleton-loader [theme]="{width: '30px', height: '13px', marginBottom: '0px'}">
</ngx-skeleton-loader>
</ng-container>
</td>
</ng-container> -->
<!-- Hauteur Column -->
<ng-container matColumnDef="hauteur">
<th class="pl-3" mat-header-cell *matHeaderCellDef> Hauteur </th>
<td class="pl-3" mat-cell *matCellDef="let element">
@if (element.hauteur) {
{{element.hauteur}}
}
@if (!element.numero) {
<ngx-skeleton-loader [theme]="{width: '30px', height: '13px', marginBottom: '0px'}">
</ngx-skeleton-loader>
}
</td>
</ng-container>
<!-- Voile Column -->
<ng-container matColumnDef="voile">
<th class="pl-3" mat-header-cell *matHeaderCellDef> Voile </th>
<td class="pl-3 text-nowrap" mat-cell *matCellDef="let element">
@if (element.voile) {
{{element.voile}} - {{element.taille}}
}
@if (!element.numero) {
<ngx-skeleton-loader [theme]="{width: '30px', height: '13px', marginBottom: '0px'}">
</ngx-skeleton-loader>
}
</td>
</ng-container>
<!-- Taille Column
<ng-container matColumnDef="taille">
<th class="pl-3" mat-header-cell *matHeaderCellDef> Taille </th>
<td class="text-nowrap text-end pl-3 pr-3" mat-cell *matCellDef="let element">
<ng-container *ngIf="element.taille">{{element.taille}}</ng-container>
<ng-container *ngIf="!element.numero">
<ngx-skeleton-loader [theme]="{width: '30px', height: '13px', marginBottom: '0px'}">
</ngx-skeleton-loader>
</ng-container>
</td>
</ng-container> -->
<!-- Categorie Column -->
<ng-container matColumnDef="categorie">
<th class="pl-3" mat-header-cell *matHeaderCellDef> Categorie </th>
<td class="pl-3 text-nowrap" mat-cell *matCellDef="let element">
@if (element.categorie) {
{{element.categorie}}{{element.categorie && element.module ? ' - ' : ''}}{{element.module}}
}
@if (!element.numero) {
<ngx-skeleton-loader [theme]="{width: '30px', height: '13px', marginBottom: '0px'}">
</ngx-skeleton-loader>
}
</td>
</ng-container>
<!-- Module Column
<ng-container matColumnDef="module">
<th class="pl-3" mat-header-cell *matHeaderCellDef> Module </th>
<td class="pl-3 text-nowrap" mat-cell *matCellDef="let element">
<ng-container *ngIf="element.module">{{element.module}}</ng-container>
<ng-container *ngIf="!element.numero">
<ngx-skeleton-loader [theme]="{width: '30px', height: '13px', marginBottom: '0px'}">
</ngx-skeleton-loader>
</ng-container>
</td>
</ng-container> -->
<!-- Zone Column -->
<ng-container matColumnDef="zone">
<th class="pl-3" mat-header-cell *matHeaderCellDef> Zone </th>
<td class="pl-3 text-nowrap" mat-cell *matCellDef="let element">
@if (element.zone) {
{{element.zone}}
}
@if (!element.numero) {
<ngx-skeleton-loader [theme]="{width: '30px', height: '13px', marginBottom: '0px'}">
</ngx-skeleton-loader>
}
</td>
</ng-container>
<!-- Participants Column -->
<ng-container matColumnDef="participants">
<th class="pl-3" mat-header-cell *matHeaderCellDef> Participants </th>
<td class="pl-3 pr-3" mat-cell *matCellDef="let element">
@if (element.participants) {
{{element.participants}}
}
@if (!element.numero) {
<ngx-skeleton-loader [theme]="{width: '30px', height: '13px', marginBottom: '0px'}">
</ngx-skeleton-loader>
}
</td>
</ng-container>
<!-- Programme Column -->
<ng-container matColumnDef="programme">
<th class="pl-3" mat-header-cell *matHeaderCellDef> Programme </th>
<td class="pl-3" mat-cell *matCellDef="let element">
@if (element.programme) {
{{element.programme}}
}
@if (!element.numero) {
<ngx-skeleton-loader [theme]="{width: '30px', height: '13px', marginBottom: '0px'}">
</ngx-skeleton-loader>
}
</td>
</ng-container>
<!-- Accessoires Column -->
<ng-container matColumnDef="accessoires">
<th class="pl-3" mat-header-cell *matHeaderCellDef> Accessoires </th>
<td class="pl-3 text-nowrap" mat-cell *matCellDef="let element">
@if (element.accessoires) {
{{element.accessoires}}
}
@if (!element.numero) {
<ngx-skeleton-loader [theme]="{width: '30px', height: '13px', marginBottom: '0px'}">
</ngx-skeleton-loader>
}
</td>
</ng-container>
<!-- Dossier Column
<ng-container matColumnDef="dossier">
<th class="pl-3" mat-header-cell *matHeaderCellDef> Dossier </th>
<td class="pl-3 text-nowrap" mat-cell *matCellDef="let element">
<ng-container *ngIf="element.dossier">{{element.dossier}}</ng-container>
<ng-container *ngIf="!element.numero">
<ngx-skeleton-loader [theme]="{width: '30px', height: '13px', marginBottom: '0px'}">
</ngx-skeleton-loader>
</ng-container>
</td>
</ng-container> -->
<!-- Video Column
<ng-container matColumnDef="video">
<th class="pl-3" mat-header-cell *matHeaderCellDef> Video </th>
<td class="pl-3 text-nowrap" mat-cell *matCellDef="let element">
@if (element.video) {
{{element.video}}
}
@if (!element.numero) {
<ngx-skeleton-loader [theme]="{width: '30px', height: '13px', marginBottom: '0px'}">
</ngx-skeleton-loader>
}
</td>
</ng-container> -->
<!-- Files Column -->
<ng-container matColumnDef="files">
<th class="pl-3" mat-header-cell *matHeaderCellDef> Fichiers </th>
<td class="pl-3 text-nowrap" mat-cell *matCellDef="let element">
@if (element.files) {
@for (file of element.files; track $index) {
@switch (file.type) {
@case ('video') {
<button mat-button (click)="openViewDialog(element)" color="navy-light">
<mat-icon fontIcon="videocam me-2"></mat-icon> {{ file.type }}
</button>
}
@case ('image') {
<button mat-button (click)="openViewDialog(element)" color="navy-light">
<mat-icon fontIcon="image me-2"></mat-icon> {{ file.type }}
</button>
}
@case ('kml') {
<button mat-button (click)="openViewDialog(element)" color="navy-light">
<mat-icon fontIcon="language me-2"></mat-icon> {{ file.type }}
</button>
}
@default {
<button mat-button (click)="openViewDialog(element)" color="navy-light">
<mat-icon fontIcon="description me-2"></mat-icon> {{ file.type }}
</button>
}
}
}
}
@if (!element.numero) {
<ngx-skeleton-loader [theme]="{width: '30px', height: '13px', marginBottom: '0px'}"></ngx-skeleton-loader>
}
</td>
</ng-container>
<!-- Actions Column -->
<ng-container matColumnDef="actions">
<th class="pl-3 text-center" mat-header-cell *matHeaderCellDef> {{ showAdd ? 'Actions' : 'Favoris' }} </th>
<td class="pl-2 text-nowrap text-center" mat-cell *matCellDef="let element">
@if (element.numero) {
<button [hidden]="!showAdd" mat-icon-button [matMenuTriggerFor]="actionMenu"
aria-label="Actions sur le saut">
<mat-icon>more_vert</mat-icon>
</button>
<mat-menu [hidden]="!showAdd" #actionMenu="matMenu">
<button mat-menu-item [routerLink]="['/jump', element.slug]">
<mat-icon aria-label="Voir" fontIcon="visibility" color="info" class="mini"></mat-icon>
<span class="text-info">Détails</span>
</button>
<button mat-menu-item (click)="openEditDialog(element)">
<mat-icon aria-label="Modifier" fontIcon="edit" color="primary" class="mini"></mat-icon>
<span class="text-primary">Modifier</span>
</button>
<button mat-menu-item (click)="openDeleteDialog(element)">
<mat-icon aria-label="Supprimer" fontIcon="delete" color="raspberry" class="mini"></mat-icon>
<span class="text-raspberry">Supprimer</span>
</button>
</mat-menu>
}
@if (!element.numero) {
<ngx-skeleton-loader [hidden]="!showAdd" appearance="circle"
[theme]="{width: '32px', height: '32px', marginBottom: '0px', marginRight: '7px'}">
</ngx-skeleton-loader>
<ngx-skeleton-loader [hidden]="!showAdd" appearance="circle"
[theme]="{width: '32px', height: '32px', marginBottom: '0px', marginRight: '7px'}">
</ngx-skeleton-loader>
<ngx-skeleton-loader appearance="circle"
[theme]="{width: '20px', height: '20px', marginBottom: '0px', marginRight: '7px'}">
</ngx-skeleton-loader>
}
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns; sticky: true" class="accent"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;" class="text-middle accent"></tr>
</table>
@@ -0,0 +1,29 @@
/* TODO(mdc-migration): The following rule targets internal classes of card that may no longer apply for the MDC version. */
mat-card-content {
padding-bottom: 10px;
}
.main-page {
.feed-toggle {
margin-bottom: -1px;
}
.sidebar {
padding: 5px 10px 10px;
background: #f3f3f3;
border-radius: 4px;
p {
margin-bottom: .2rem;
}
}
}
.table.table-condensed.mat-mdc-table {
tr {
height: inherit;
}
td {
vertical-align: middle;
}
}
.mat-mdc-progress-spinner {
display: inline-block;
}
@@ -0,0 +1,472 @@
import { AfterContentChecked, Component, EventEmitter, Input, Output, OnChanges, OnDestroy, ViewChild } from '@angular/core';
import { CommonModule, DecimalPipe, DatePipe } from '@angular/common';
import { UntypedFormBuilder, UntypedFormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms';
import { Router, RouterLink } from '@angular/router';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatDialog, MatDialogModule } from '@angular/material/dialog';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { MatMenuModule } from '@angular/material/menu';
import { MatNativeDateModule } from '@angular/material/core';
import { MatPaginator, MatPaginatorIntl, MatPaginatorModule } from '@angular/material/paginator';
import { MatProgressBarModule } from '@angular/material/progress-bar';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatSort, MatSortModule } from '@angular/material/sort';
import { MatTableDataSource, MatTableModule } from '@angular/material/table';
import { MatSnackBar, MatSnackBarHorizontalPosition, MatSnackBarModule, MatSnackBarVerticalPosition } from '@angular/material/snack-bar';
import { Observable, Subscription } from 'rxjs';
import { take } from 'rxjs/operators';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { FrenchPaginator } from 'src/app/components/shared/french-paginator.component';
import { ColumnDefinition, Errors, Jump, JumpByDay, JumpFile, JumpFileType, JumpList, JumpListConfig, jumpColumns } from 'src/app/core/models';
import { JumpsService, JumpTableService } from 'src/app/core/services';
import { JumpAddDialogComponent, JumpDeleteDialogComponent, JumpEditDialogComponent, JumpViewDialogComponent } from 'src/app/components/logbook/dialogs'
@Component({
standalone: true,
imports: [
CommonModule, DecimalPipe, DatePipe, RouterLink, FormsModule, ReactiveFormsModule, NgxSkeletonLoaderModule,
MatButtonModule, MatCardModule, MatDialogModule,
MatFormFieldModule, MatIconModule, MatInputModule, MatNativeDateModule, MatMenuModule,
MatPaginatorModule, MatProgressBarModule, MatProgressSpinnerModule, MatSnackBarModule, MatSortModule, MatTableModule,
JumpAddDialogComponent, JumpDeleteDialogComponent, JumpEditDialogComponent, JumpViewDialogComponent
],
selector: 'app-jump-table',
styleUrl: './jump-table.component.scss',
templateUrl: './jump-table.component.html',
providers: [{provide: MatPaginatorIntl, useClass: FrenchPaginator}]
})
export class JumpTableComponent implements OnChanges, OnDestroy, AfterContentChecked {
private _jumps: Subscription = new Subscription();
private _subscriptions: Array<Subscription> = [];
//private _jump: Subscription = new Subscription();
private _query: JumpListConfig = { type: 'all', filters: {} };
//private _lastjump: Jump = {} as Jump;
private _currentPage = 1;
public errors!: Errors;
public jump: Jump = {} as Jump;
public jumps!: MatTableDataSource<Jump>;
public jumpsCount = 0;
/* public displayedColumns: string[] = [
'numero', 'date', 'lieu', 'oaci', 'aeronef',
'imat', 'hauteur', 'voile', 'taille', 'categorie',
'module', 'participants', 'programme', 'accessoires',
'zone', 'files', 'actions'
];*/
public displayedColumns: string[] = [
'numero', 'date', 'lieu', 'aeronef',
'hauteur', 'voile', 'categorie',
'participants', 'zone', 'accessoires',
'files', 'actions'
];
//public displayedColumns: string[] = jumpColumns.map((col) => col.key)
public columnsSchema: Array<ColumnDefinition> = jumpColumns;
public searchForm: UntypedFormGroup;
public loading = false;
public isSubmitting = false;
public isDeleting = false;
public tableRefresh = false;
public jumpRefresh = false;
//public valid: any = {};
@ViewChild(MatSort) sort!: MatSort;
@ViewChild(MatPaginator) paginator!: MatPaginator;
constructor(
private router: Router,
private _jumpsService: JumpsService,
private _jumpTableService: JumpTableService,
private dialog: MatDialog,
private fb: UntypedFormBuilder,
private snackBar: MatSnackBar
) {
this._resetErrors();
this.searchForm = this.fb.group({
//numero: ['', Validators.required]
numero: ''
});
}
@Input() title = '';
@Input() limit = 0;
@Input() showAdd = false;
@Input() isUser = false;
@Input()
set config(config: JumpListConfig) {
if (config) {
this._query = config;
this._currentPage = 1;
}
}
get config(): JumpListConfig {
return this._query;
}
@Output() countChange: EventEmitter<number> = new EventEmitter();
ngOnChanges() {
this.loading = true;
this._query = this.config;
this.runQuery();
//console.log('ngOnChanges event in jump-table component');
}
ngOnDestroy() {
this._jumps.unsubscribe();
this._subscriptions.forEach(subscription => {
subscription.unsubscribe();
});
}
ngAfterContentChecked() {
if (this.jumpRefresh != this._jumpTableService.jumpRefresh) {
this.jumpRefresh = this._jumpTableService.jumpRefresh;
this.runQuery();
//console.log('table has been refreshed in jump-table component');
}
}
runQuery() {
/*
const jump$: Observable<Jump> = this._jumpsService.getLastJump();
this._jump = jump$.subscribe((jump) => {
this._lastjump = jump;
});
*/
if (this.limit) {
this._query.filters.limit = this.limit;
this._query.filters.offset = (this.limit * (this._currentPage - 1));
} else {
this._query.filters.limit = 0;
this._query.filters.offset = 0;
}
/*
this._query.filters.numeroRangeStart = 1;
this._query.filters.numeroRangeEnd = 0;
this._query.filters.tailleRangeStart = 135;
this._query.filters.tailleRangeEnd = 230;
this._query.filters.participantsRangeStart = 1;
this._query.filters.participantsRangeEnd = 10;
this._query.filters.hauteurRangeStart = 0;
this._query.filters.hauteurRangeEnd = 8000;
*/
const jumps$: Observable<JumpList> = this._jumpsService.query(this._query);
this._jumps = jumps$.subscribe({
next: (data: JumpList) => {
//this.updateJumps(data.jumps);
//this.linkSkydiverIdJumps();
//this.getKmlJumpFiles(data.jumps);
this.loading = false;
this.jumps = new MatTableDataSource<Jump>(data.jumps);
this.jumpsCount = data.jumpsCount;
this.countChange.emit(this.jumpsCount);
this.paginator.length = data.jumpsCount;
this.jumps.paginator = this.paginator;
this.jumps.sort = this.sort;
this._resetErrors();
},
error: (err) => {
this.errors = err;
}
});
}
linkSkydiverIdJumps() {
const config: JumpListConfig = { type: 'all', filters: {} } as JumpListConfig;
config.filters.limit = 200;
config.filters.offset = 0;
config.filters.dateRangeStart = '2024-01-01 00:00:00';
//const year = new Date().getFullYear();
//config.filters.dateRangeEnd = `${year}-12-31 23:59:59`;
config.filters.dateRangeEnd = '2024-12-31 23:59:59';
//console.log(config.filters);
const subscription: Subscription = this._jumpsService.getAllByDay(config)
.pipe(take(1))
.subscribe({
next: (days) => {
this._resetErrors();
//console.log('getAllByDay : ', days);
days.forEach(day => {
this.loadSkydiverIdJumps(day, config);
});
},
error: (err) => {
this.errors = err;
}
});
this._subscriptions.push(subscription);
}
loadSkydiverIdJumps(day: JumpByDay, config: JumpListConfig) {
config.filters.dateRangeStart = `${day.jumps[0].date!.substring(0, 10)} 00:00:00`;
config.filters.dateRangeEnd = `${day.jumps[0].date!.substring(0, 10)} 23:59:59`;
const subscription: Subscription = this._jumpsService.getAllFromSkydiverIdApi(config)
.pipe(take(1))
.subscribe({
next: (data) => {
if (data.items.length) {
const diff = day.count - data.items.length;
if (diff < 0) {
console.error(`${diff} ${diff > 1 ? 'données' : 'donnée'} altimètre de trop le ${day.jumps[0].date!.substring(0, 10)}`);
} else {
if (diff > 0) {
console.error(`${diff}/${day.count} ${diff > 1 ? 'données altimètre manquantes' : 'donnée altimètre manquante'} le ${day.jumps[0].date!.substring(0, 10)}`, day.jumps, data.items);
}
console.log(`${day.count} ${day.count > 1 ? 'sauts' : 'saut'} le ${data.items[0].date!.substring(0, 10)}`, day.jumps.sort((a, b) => b.numero! - a.numero!), data.items);
data.items.map((jump, index) => {
const file: JumpFile = {
name: jump.name,
path: `/Volumes/Storage/Skydive/X2_Logs/${day.jumps[index].date!.substring(0, 4)}/`,
type: JumpFileType.CSV
} as JumpFile;
this._subscriptions.push(
this._jumpsService.saveX2Data(day.jumps[index].slug!, file, jump)
.pipe(take(1))
.subscribe({
next: (jump) => {
this._resetErrors();
console.log(`Le fichier ${file.name} a été ajouté au saut n°${jump.numero} !`, 'OK');
},
error: (err) => {
this.errors = err;
}
})
);
return jump;
});
}
//console.log(`${day.count} ${day.count > 1 ? 'sauts' : 'saut'} le ${day.jumps[0].date!.substring(0, 10)}`);
//console.log(`${data.items.length} ${data.items.length > 1 ? 'sauts' : 'saut'} le ${data.items[0].date!.substring(0, 10)}`, data);
} else {
console.info('Aucune données altimètre pour le ', day.jumps[0].date!.substring(0, 10));
}
},
error: (err) => {
console.error('getAllFromSkydiverIdApi error');
this.errors = err;
}
});
this._subscriptions.push(subscription);
}
updateJumps(jumps: Array<Jump>) {
jumps.map(jump => {
jump.files = [];
if (jump.dossier !== "" && jump.video !== "") {
const file: JumpFile = {
name: jump.video!,
path: jump.dossier!,
type: JumpFileType.VIDEO
} as JumpFile;
this._subscriptions.push(
this._jumpsService.saveFile(jump.slug, file)
.pipe(take(1))
.subscribe({
next: (file) => {
this._resetErrors();
jump.files.push(file);
//console.log(`Le fichier ${file.name} a été ajouté au saut n°${jump.numero} !`, 'OK');
},
error: (err) => {
this.errors = err;
}
})
);
}
return jump;
});
}
getKmlJumpFiles(jumps: Array<Jump>) {
jumps.map(jump => {
if (jump.x2data !== null) {
//console.log(`Le fichier ${jump.x2data.name} existe pour le saut n°${jump.numero} !`, `https://skydiver.id/api/jump/${jump.x2data.id}/kml`);
const file: JumpFile = {
name: jump.x2data.name.substring(0, -4) + '.kml',
path: `/X2_Kmls/${jump.date.substring(0, 4)}/`,
type: JumpFileType.KML
} as JumpFile;
this._subscriptions.push(
this._jumpsService.saveKml(jump.slug, file)
.pipe(take(1))
.subscribe({
next: (file) => {
this._resetErrors();
console.log(`Le fichier ${file.path}${file.name} a été ajouté au saut n°${jump.numero} !`, 'OK');
},
error: (err) => {
this.errors = err;
}
})
);
}
return jump;
});
}
deleteJump(slug: string) {
this.isDeleting = true;
this._subscriptions.push(
this._jumpsService.destroy(slug)
.pipe(take(1))
.subscribe(() => {
this.router.navigateByUrl('/');
})
);
}
openDeleteDialog(jump: Jump): void {
const dialogRef = this.dialog.open(JumpDeleteDialogComponent, {
width: '60vw',
data: jump
});
this._subscriptions.push(
dialogRef.afterClosed()
.pipe(take(1))
.subscribe(result => {
if (result != undefined) {
this._onDelete(result.slug);
}
})
);
}
openEditDialog(jump: Jump): void {
const dialogRef = this.dialog.open(JumpEditDialogComponent, {
width: '70vw',
data: jump
});
this._subscriptions.push(
dialogRef.afterClosed()
.pipe(take(1))
.subscribe(result => {
if (result != undefined) {
this._onEdit(result);
}
})
);
}
openViewDialog(jump: Jump): void {
const dialogRef = this.dialog.open(JumpViewDialogComponent, {
width: '60vw',
data: jump
});
dialogRef.afterClosed();
}
getErrorMessage() {
if (this.searchForm.controls['numero'].errors !== null) {
return 'Vous devez indiquer une valeur';
}
return '';
}
submitForm() {
if (this.searchForm.valid) {
this._query.filters.numero = this.searchForm.value.numero;
this._resetErrors();
this.runQuery();
}
}
private _onDelete(slug: string): void {
try {
this.isDeleting = true;
this._subscriptions.push(
this._jumpsService.destroy(slug)
.pipe(take(1))
.subscribe({
next: () => {
this._resetErrors();
this._openSnackBar(`Le saut a été supprimé !`, 'OK');
this.runQuery();
this.tableRefresh = !this.tableRefresh;
this._jumpTableService.updateTableRefresh(this.tableRefresh);
},
error: (err) => {
this.errors = err;
}
})
);
} catch (error) {
console.error(error);
}
}
private _onEdit(jump: Jump): void {
try {
this.isSubmitting = true;
this._subscriptions.push(
this._jumpsService.update(jump)
.pipe(take(1))
.subscribe({
next: (jump) => {
this._resetErrors();
this._openSnackBar(`Le saut n°${jump.numero} a été mis à jour !`, 'OK');
this.runQuery();
this.tableRefresh = !this.tableRefresh;
this._jumpTableService.updateTableRefresh(this.tableRefresh);
},
error: (err) => {
this.errors = err;
this.isSubmitting = false;
}
})
);
} catch (error) {
console.error(error);
}
}
private _openSnackBar(content: string, title: string) {
this.snackBar.open(content, title, {
horizontalPosition: <MatSnackBarHorizontalPosition>'end',
verticalPosition: <MatSnackBarVerticalPosition>'bottom',
duration: 4000,
});
}
private _resetErrors(): void {
this.errors = { errors: {} };
}
/*
disableSubmit(id: number) {
if (this.valid[id]) {
return Object.values(this.valid[id]).some((item) => item === false)
}
return false
}
inputHandler(e: any, id: number, key: string) {
if (!this.valid[id]) {
this.valid[id] = {}
}
this.valid[id][key] = e.target.validity.valid
}
*/
/*
isAllSelected() {
return this.jumps.data.every((item) => item.isSelected)
}
isAnySelected() {
return this.jumps.data.some((item) => item.isSelected)
}
selectAll(event: any) {
this.jumps.data = this.jumps.data.map((item) => ({
...item,
isSelected: event.checked,
}))
}
*/
}