27 Commits

Author SHA1 Message Date
julien 69cf4548d2 refactor(naming): harmonize service file names and API domain prefixes
- Rename hwclan.service.ts → hw-clan.service.ts, hwmember.service.ts → hw-member.service.ts (Angular Style Guide kebab-case)
- Replace _apiVersion (/v1, /v2) with _apiDomain (/skydive, /cms, /herowars) across all feature services
- Update herowars barrel to reference renamed files
- Update CLAUDE.md to reflect corrected path examples
2026-04-26 00:39:03 +02:00
julien 5a7baf4d94 chore(jumps): remove dead feed type from query method
The /feed branch was never called — all consumers pass type 'all'.
Mirrors removal of the skydive /jumps/feed backend route.
2026-04-26 00:16:35 +02:00
julien c7d21bda30 docs: add CLAUDE.md with project guidance for AI assistants 2026-04-25 17:23:33 +02:00
julien 3ecc83458f chore(angular): add @angular/localize/init polyfill
Required for fr-FR locale support that the app already configures
via registerLocaleData and LOCALE_ID provider in app.config.ts.
2026-04-25 17:23:25 +02:00
julien d6c864d6d1 fix(menu): correct "shydive" typo to "skydive" in admin menu
Eight items under the admin menu had a misspelled parent state,
breaking the parent/child relationship for the skydive submenu.
Also reparents the qcm submenu to skydive (was sitting at the root).
2026-04-25 17:23:03 +02:00
julien 223955eb84 refactor(environment): rename config keys to camelCase
Aligns environment.* shape with TypeScript convention.
Renames: api_url -> apiBaseUrl, api_key -> apiKey,
use_api_key -> useApiKey, refresh_interval -> refreshInterval,
google_map_api_key -> googleMapApiKey.

The local environment.{ts,development.ts,local.ts} files are
gitignored and have already been migrated; this commit updates
the tracked example file and the only consumer (apiInterceptor).
2026-04-25 17:22:49 +02:00
julien c32191b37d Material Upgrade 2026-03-30 02:09:35 +02:00
julien bd8ed94b1d Angular update to v18 2026-03-30 01:52:34 +02:00
julien 518168f225 Angular upgrade to v18 2026-03-30 01:48:45 +02:00
julien f9ee9ed799 Packages update 2026-03-30 01:13:03 +02:00
julien 59361d46a3 Menu link update 2026-03-30 01:05:32 +02:00
julien 9d5d0fca56 Environments update 2026-03-29 23:50:56 +02:00
julien f5e8c765b7 Environments update 2026-03-29 23:44:29 +02:00
julien 8a602bd2db Refactoring 2026-03-29 23:23:33 +02:00
julien 43d61fcf16 Environments Update 2026-03-29 23:20:02 +02:00
julien 86032382d6 Add gitignore for environments 2026-03-29 23:05:25 +02:00
julien 8a1c19e011 Ajout de composants Hero Wars 2025-11-27 10:35:38 +01:00
Rampeur 8e3bd16b60 Ajout d'un pipe 'floor' 2025-09-23 22:30:43 +02:00
Rampeur 5340e737c6 Ajout du composant 'demo' 2025-09-23 22:28:15 +02:00
Rampeur 8b098f7545 Mise à jour graphique 2025-09-23 22:25:08 +02:00
Rampeur 65ae9fce4b Mise à jour de services et resolvers 2025-08-29 16:07:08 +02:00
Rampeur a0a194306f Mise à jour du menu de la sidebar 2025-08-29 16:05:37 +02:00
Rampeur 452475b26e Ajouts et modifications de models 2025-08-29 16:04:20 +02:00
Rampeur ed6104ed21 Mise à jour articles et produits 2025-08-19 05:12:16 +02:00
Rampeur 977104aac2 Mise à jour du layout 2025-08-13 01:28:11 +02:00
Rampeur 0a6cbc0c00 Import des sources angular à partir de 'headup_app' 2025-08-11 23:26:29 +02:00
Rampeur 7dcb426ef5 Suppression de fichiers 2025-08-11 22:57:30 +02:00
498 changed files with 69374 additions and 34 deletions
+16
View File
@@ -0,0 +1,16 @@
# Editor configuration, see https://editorconfig.org
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 4
insert_final_newline = true
trim_trailing_whitespace = true
[*.ts]
quote_type = single
[*.md]
max_line_length = off
trim_trailing_whitespace = false
+46
View File
@@ -0,0 +1,46 @@
{
"root": true,
"ignorePatterns": [
"projects/**/*"
],
"overrides": [{
"files": [
"*.ts"
],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:@angular-eslint/recommended",
"plugin:@angular-eslint/template/process-inline-templates"
],
"rules": {
"@angular-eslint/directive-selector": [
"error",
{
"type": "attribute",
"prefix": "app",
"style": "camelCase"
}
],
"@angular-eslint/component-selector": [
"error",
{
"type": "element",
"prefix": "app",
"style": "kebab-case"
}
]
}
},
{
"files": [
"*.html"
],
"extends": [
"plugin:@angular-eslint/template/recommended",
"plugin:@angular-eslint/template/accessibility"
],
"rules": {}
}
]
}
+3
View File
@@ -53,3 +53,6 @@ sonar.properties
**/*.copy.ts
**/*.copy.scss
**/*.copy.html
.next/
next-env.d.ts
+72
View File
@@ -0,0 +1,72 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Commands
- `npm start` — dev server (production config) on port 4200
- `npm run dev` — dev server with `APP_ENV=development` on port 4300, uses `environment.development.ts`
- `npm run local` — dev server with `APP_ENV=local` on port 4400, uses `environment.local.ts`
- `npm run serve` — alias for local config on port 4201
- `npm run build` — production build to `dist/adastra_angular/`
- `npm run watch` — incremental build in development configuration
- `npm run lint` — angular-eslint over `src/**/*.ts` and `src/**/*.html`
- `npm test` — Karma + Jasmine (single spec: `ng test --include src/path/to/file.spec.ts`)
Node engines: `^18.19.1 || ^20.11.1`. Angular 18, TypeScript 5.5, strict mode with `strictTemplates`.
## Environments
`src/environments/environment.ts` is the default (production). `environment.development.ts` and `environment.local.ts` are swapped in via `fileReplacements` in `angular.json` for their respective configurations. `environment.example.ts` documents the expected shape (`apiBaseUrl`, `apiKey`, `useApiKey`, `refreshInterval`, `googleMapApiKey`). Never commit real keys to the development/local files — the production `environment.ts` currently has real keys checked in; be mindful.
## Architecture
Standalone Angular 18 app (no NgModules) wired up in [src/app/app.config.ts](src/app/app.config.ts). It provides the router with `withViewTransitions()`, three HTTP interceptors, charts, French locale (`fr-FR`), and an `APP_INITIALIZER` that eagerly fetches the current user when a JWT is present in localStorage.
### Routing split
Routes are composed in [src/app/app.routes.ts](src/app/app.routes.ts) from two arrays:
- [src/app/routes/auth.routes.ts](src/app/routes/auth.routes.ts) — `authGuard`-protected routes. Skydive admin sections (`aeronefs`, `canopies`, `dashboard`, `dropzones`, `jumps`) additionally require `adminGuard`.
- [src/app/routes/noauth.routes.ts](src/app/routes/noauth.routes.ts) — public pages and auth-entry routes (`login`, `register`) gated by `noauthGuard` (blocks entry when already authenticated).
Resolvers under [src/app/core/resolvers/](src/app/core/resolvers/) pre-fetch data for each route — new route components should generally follow this pattern rather than fetching in `ngOnInit`.
### HTTP pipeline
Three interceptors chained in order ([src/app/core/interceptors/](src/app/core/interceptors/)):
1. `apiInterceptor` — prepends `environment.apiBaseUrl` to every request URL. **Consequence:** services call relative paths like `/cms/user`, never absolute URLs.
2. `tokenInterceptor` — attaches `Authorization: Token <jwt>` when a token exists.
3. `errorInterceptor` — unwraps `err.error` and rethrows.
`ApiService` ([api.service.ts](src/app/core/services/api.service.ts)) is a thin `HttpClient` wrapper; most feature services call it with a versioned prefix (e.g., `_apiDomain = '/skydive'` or `/cms`). The API version is per-service, not global.
### Auth flow
`JwtService` stores the token in `localStorage['jwtToken']`. `UserService` exposes `currentUser` (BehaviorSubject) and `isAuthenticated` (ReplaySubject) observables consumed by guards and the layout. The `APP_INITIALIZER` in `app.config.ts` calls `userService.getCurrentUser()` if a token exists, so guards can rely on `isAuthenticated` being populated on first navigation. Admin role check is `currentUser.role === 'Admin'`.
### Layout
[AppComponent](src/app/app.component.ts) renders only `FullComponent` ([src/app/components/shared/layout/full.component.ts](src/app/components/shared/layout/full.component.ts)), which owns the Material sidenav + toolbar shell and swaps menu items between `MENUITEMS` and `MENUITEMSADMIN` defined in [src/app/components/shared/menu-items.ts](src/app/components/shared/menu-items.ts) based on role. Route changes render inside its `<router-outlet>`.
### Feature areas
The app spans four mostly-independent feature domains, mirroring the backend's route grouping:
- **E-commerce** — `products`, `product/:slug`, `products/:category` (public + auth variants). Categories are baked into menu items, not dynamic. Backend counterpart: `/api/ecommerce/*`.
- **CMS** — `pages`, `page/:slug`, `home`, profiles, articles. Backend counterpart: `/api/cms/*`.
- **Skydive** — `skydive/*` routes, services in [src/app/core/services/skydive/](src/app/core/services/skydive/), QCM exam data in `src/app/components/qcm/*.json` and `src/qcm-bpa.json`. Backend counterpart: `/api/skydive/*`.
- **Hero Wars** — analytics for a game guild, services in [src/app/core/services/herowars/](src/app/core/services/herowars/), weekly data snapshots in [src/files-data/](src/files-data/) (`Week_XX` folders and `hw-*.json` files are static data shipped as assets). Backend counterpart: `/api/herowars/*`.
### Path aliases
Configured in [tsconfig.json](tsconfig.json) and used everywhere — prefer them over relative paths:
`@components`, `@services`, `@models`, `@guards`, `@interceptors`, `@resolvers`, `@viewmodels`, `@interfaces`, `@constants`, `@core/*`, `@environments/*`, `@data/*` (→ `src/files-data/*`), `@assets/*`, `@styles/*`.
Each alias points at an `index.ts` barrel — when adding a new component/service/model, also export it from the appropriate barrel or it won't be importable via the alias.
### Styling
SCSS with `includePaths: ["src/styles"]` so imports like `@use 'variables'` resolve. Global Chartist styles are registered in `angular.json` build options. Material 18 with French date locale and `DD/MM/YYYY` format configured in `AppComponent`'s providers.
## Related service
The backend API lives in a sibling repo at `/Users/julien/Sites/adastra_api` (Express + Sequelize, additional working directory). Routes are grouped under `src/routes/api/` into the same four domains as the frontend: `skydive`, `cms`, `ecommerce`, `herowars`. The `v1`, `v2`, `v3` folders alongside them are legacy backups slated for removal — ignore them. When a task spans both repos (e.g., adding an endpoint + consumer), check there as well.
+19 -34
View File
@@ -1,42 +1,27 @@
# AdAstra_App
# Ad Astra
## Libraries and Frameworks
This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 17.2.2.
| Name | |
| --------------------------------------------- | ---------------------------------- |
| [TypeScript](https://www.typescriptlang.org/) | v5.5.x |
| [Next.js](https://nextjs.org/) | v15.x |
| [React](https://react.dev/) | v18.x |
| [Conform](https://conform.guide/) | form validation library |
| [Zod](https://zod.dev/) | schema validation library |
| [unified](https://unifiedjs.com/) | markdown to html converter |
| [OpenAPI TypeScript](https://openapi-ts.dev/) | type generator from OpenAPI schema |
## Development server
## Getting Started
Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files.
```bash
# copy env
cp .env.example .env.local
## Code scaffolding
# set API_BASE_URL variable in .env.local to your backend api endpoint
Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`.
# run app
npm run dev
```
## Build
## Directory structure
Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory.
```plaintext
.
├── api/ # api schema
├── public/ # static assets
└── src/
├── app/ # web routes
├── config/ # global configuration and constants
├── generated/ # automatically generated codes
├── modules/
│ ├── common/ # common (feature-independent) components
│ └── features/ # feature-specific components
├── styles/ # global style sheets
└── utils/ # utilities
```
## Running unit tests
Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
## Running end-to-end tests
Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities.
## Further help
To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page.
+172
View File
@@ -0,0 +1,172 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"cli": {
"packageManager": "npm",
"analytics": false,
"schematicCollections": [
"@angular-eslint/schematics"
]
},
"newProjectRoot": "projects",
"projects": {
"adastra_angular": {
"projectType": "application",
"schematics": {
"@schematics/angular:component": {
"style": "scss"
}
},
"i18n": {
"sourceLocale": "fr"
},
"root": "",
"sourceRoot": "src",
"prefix": "app",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:application",
"options": {
"outputPath": {
"base": "dist/adastra_angular"
},
"index": "src/index.html",
"tsConfig": "tsconfig.app.json",
"polyfills": [
"@angular/localize/init",
"zone.js"
],
"inlineStyleLanguage": "scss",
"assets": [
"src/favicon.ico",
"src/assets",
"src/manifest.webmanifest",
{
"glob": "_redirects",
"input": "src",
"output": "/"
}
],
"styles": [
"node_modules/chartist/dist/index.scss",
"src/styles/styles.scss"
],
"stylePreprocessorOptions": {
"includePaths": ["src/styles"]
},
"scripts": [],
"browser": "src/main.ts"
},
"configurations": {
"production": {
"extractLicenses": true,
"namedChunks": false,
"optimization": true,
"outputHashing": "all",
"sourceMap": false,
"budgets": [
{
"type": "initial",
"maximumWarning": "500kb",
"maximumError": "1mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "2kb",
"maximumError": "4kb"
}
]
},
"development": {
"extractLicenses": false,
"namedChunks": true,
"optimization": false,
"sourceMap": true,
"budgets": [
{
"type": "anyComponentStyle",
"maximumWarning": "6kb"
}
],
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.development.ts"
}
]
},
"local": {
"extractLicenses": false,
"namedChunks": true,
"optimization": false,
"sourceMap": true,
"budgets": [
{
"type": "anyComponentStyle",
"maximumWarning": "6kb"
}
],
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.local.ts"
}
]
}
},
"defaultConfiguration": "production"
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"configurations": {
"production": {
"buildTarget": "adastra_angular:build:production"
},
"development": {
"buildTarget": "adastra_angular:build:development"
},
"local": {
"buildTarget": "adastra_angular:build:local"
}
},
"defaultConfiguration": "development"
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"buildTarget": "adastra_angular:build"
}
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"polyfills": [
"zone.js",
"zone.js/testing"
],
"tsConfig": "tsconfig.spec.json",
"inlineStyleLanguage": "scss",
"assets": [
"src/favicon.ico",
"src/assets",
"src/manifest.webmanifest"
],
"styles": [
"src/styles/styles.scss"
],
"scripts": []
}
},
"lint": {
"builder": "@angular-eslint/builder:lint",
"options": {
"lintFilePatterns": [
"src/**/*.ts",
"src/**/*.html"
]
}
}
}
}
}
}
+14995
View File
File diff suppressed because it is too large Load Diff
+67
View File
@@ -0,0 +1,67 @@
{
"name": "adastra-app",
"version": "1.0.0",
"author": "Solide Apps <admin@unespace.com>",
"contributors": [
"Julien Gautier <julien.gautier@unespace.com>"
],
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"watch": "ng build --watch --configuration development",
"serve": "ng serve --configuration local --port 4201",
"dev": "APP_ENV=development ng serve --port 4300 --configuration development",
"local": "APP_ENV=local ng serve --port 4400 --configuration local",
"lint": "ng lint",
"test": "ng test"
},
"engines": {
"node": "^18.19.1 || ^20.11.1"
},
"private": true,
"dependencies": {
"@angular/animations": "^18.2.14",
"@angular/cdk": "^18.2.14",
"@angular/common": "^18.2.14",
"@angular/compiler": "^18.2.14",
"@angular/core": "^18.2.14",
"@angular/forms": "^18.2.14",
"@angular/google-maps": "^17.3.8",
"@angular/localize": "^18.2.14",
"@angular/material": "^18.2.14",
"@angular/platform-browser": "^18.2.14",
"@angular/platform-browser-dynamic": "^18.2.14",
"@angular/router": "^18.2.14",
"bootstrap": "^5.3.7",
"chart.js": "^4.4.2",
"chartist": "^1.3.0",
"ng-chartist": "^8.2.0",
"ng2-charts": "^6.0.1",
"ngx-skeleton-loader": "^9.0.0",
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
"zone.js": "^0.14.10"
},
"devDependencies": {
"@angular-devkit/build-angular": "^18.2.21",
"@angular-eslint/builder": "18.4.3",
"@angular-eslint/eslint-plugin": "18.4.3",
"@angular-eslint/eslint-plugin-template": "18.4.3",
"@angular-eslint/schematics": "18.4.3",
"@angular-eslint/template-parser": "18.4.3",
"@angular/cli": "^18.2.21",
"@angular/compiler-cli": "^18.2.14",
"@types/jasmine": "~5.1.0",
"@typescript-eslint/eslint-plugin": "7.2.0",
"@typescript-eslint/parser": "7.2.0",
"eslint": "^8.57.0",
"jasmine-core": "~5.1.0",
"karma": "~6.4.0",
"karma-chrome-launcher": "~3.2.0",
"karma-coverage": "~2.2.0",
"karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~2.1.0",
"typescript": "~5.5.4"
}
}
+1
View File
@@ -0,0 +1 @@
/* /index.html 200
+1
View File
@@ -0,0 +1 @@
<app-full-layout [class]="appClass"></app-full-layout>
+8
View File
@@ -0,0 +1,8 @@
.grayscale {
filter: grayscale(1);
transition: all 2s ease;
}
.colored {
filter: grayscale(0);
transition: all 5s ease;
}
+29
View File
@@ -0,0 +1,29 @@
import { TestBed } from '@angular/core/testing';
import { AppComponent } from './app.component';
describe('AppComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [AppComponent],
}).compileComponents();
});
it('should create the app', () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance;
expect(app).toBeTruthy();
});
it(`should have the 'adastra_angular' title`, () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance;
expect(app.title).toEqual('adastra_angular');
});
it('should render title', () => {
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
const compiled = fixture.nativeElement as HTMLElement;
expect(compiled.querySelector('h1')?.textContent).toContain('Hello, adastra_angular');
});
});
+48
View File
@@ -0,0 +1,48 @@
import { Component, OnInit, AfterContentInit } from '@angular/core';
import { MAT_DATE_LOCALE, MAT_DATE_FORMATS, provideNativeDateAdapter } from '@angular/material/core';
import { Title } from '@angular/platform-browser';
import { FullComponent } from '@components/shared/layout';
import { UserService } from "@services";
export const MY_FORMATS = {
parse: {
dateInput: 'DD/MM/YYYY',
},
display: {
dateInput: 'DD/MM/YYYY',
monthYearLabel: 'MMM YYYY',
dateA11yLabel: 'LL',
monthYearA11yLabel: 'MMMM-YYYY',
},
};
@Component({
selector: 'app-root',
standalone: true,
imports: [
FullComponent
],
providers: [
{ provide: MAT_DATE_LOCALE, useValue: 'fr-FR' },
{ provide: MAT_DATE_FORMATS, useValue: MY_FORMATS },
provideNativeDateAdapter(MY_FORMATS)
],
templateUrl: './app.component.html',
styleUrl: './app.component.scss'
})
export class AppComponent implements OnInit, AfterContentInit {
public title = 'Ad Astra';
public appClass = 'grayscale';
constructor(private userService: UserService, private titleService: Title) { }
ngOnInit() {
this.userService.populate();
this.titleService.setTitle(this.title);
}
ngAfterContentInit() {
this.appClass = 'colored';
}
}
+40
View File
@@ -0,0 +1,40 @@
import { APP_INITIALIZER, ApplicationConfig, LOCALE_ID } from '@angular/core';
import { Title } from '@angular/platform-browser';
//import { provideRouter, withComponentInputBinding, withDebugTracing } from '@angular/router';
import { provideRouter, withViewTransitions } from '@angular/router';
import { provideAnimations } from '@angular/platform-browser/animations';
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { registerLocaleData } from '@angular/common';
import localeFr from '@angular/common/locales/fr';
import { provideCharts, withDefaultRegisterables } from 'ng2-charts';
import { EMPTY } from "rxjs";
registerLocaleData(localeFr);
import { routes } from './app.routes';
import { apiInterceptor, tokenInterceptor, errorInterceptor } from "@interceptors";
import { JwtService, UserService } from "@services";
export function initAuth(jwtService: JwtService, userService: UserService) {
return () => (jwtService.getToken() ? userService.getCurrentUser() : EMPTY);
}
export const appConfig: ApplicationConfig = {
providers: [
//provideRouter(routes, withComponentInputBinding(), withDebugTracing()),
provideRouter(routes, withViewTransitions()),
provideHttpClient(
withInterceptors([apiInterceptor, tokenInterceptor, errorInterceptor])
),
provideAnimations(),
provideCharts(withDefaultRegisterables()),
Title,
{ provide: LOCALE_ID, useValue: 'fr-FR' },
{
provide: APP_INITIALIZER,
useFactory: initAuth,
deps: [JwtService, UserService],
multi: true,
}
]
};
+5
View File
@@ -0,0 +1,5 @@
import { Routes } from '@angular/router';
import { AUTH_ROUTES, NOAUTH_ROUTES } from 'src/app/routes';
export const routes: Routes = [ ...AUTH_ROUTES, ...NOAUTH_ROUTES];
@@ -0,0 +1,49 @@
<div @flyInOut>
<div class="d-flex">
<div class="flex-fill">
<h1 class="mb-1">{{title}}</h1>
<span [hidden]="!lastJump.numero" class="me-2 fs-6">
Dernier saut enregistré : <span class="fs-5 mx-1 text-primary">{{ lastJump.numero }}<sup>ème</sup></span> à <span class="fs-5 mx-1 text-primary">{{ lastJump.lieu }}</span> le <span class="fs-5 mx-1 text-primary">{{ lastJump.date | date: 'dd/MM/yyyy' }}</span>
</span>
</div>
<span class="flex-spacer"></span>
<div class="align-self-center me-2">
<button mat-icon-button [matMenuTriggerFor]="menuAeronef" aria-label="Menu Aeronef">
<mat-icon fontIcon="more_vert"></mat-icon>
</button>
<mat-menu #menuAeronef="matMenu">
@for (menuitem of menuItems.getMenuPanel(); track menuitem) {
<button mat-menu-item>
<mat-icon [fontIcon]="menuitem.icon"></mat-icon>
<span>{{ menuitem.name }}</span>
</button>
}
</mat-menu>
</div>
</div>
<mat-divider class="my-3"></mat-divider>
<mat-card>
<mat-card-header>
<mat-card-title>{{ subtitle }}</mat-card-title>
</mat-card-header>
<mat-card-content class="mt-4">
@if (displayCharts) {
<div class="row">
<div class="col-xl-4 col-lg-5 col-md-6 col-sm-12">
<app-pie-chart [headers]="seriesHeader" [names]="seriesName" [values]="seriesValue" [colors]="seriesColor"></app-pie-chart>
</div>
<div class="col-xl-8 col-lg-7 col-md-6 col-sm-12">
<!--<app-bar-chart [headers]="seriesHeader" [names]="seriesName" [values]="seriesValue" [colors]="seriesColor" label="Nombre total de sauts" [legend]="false"></app-bar-chart>-->
<app-bars-chart [headers]="seriesHeader" [names]="seriesName" [values]="seriesRow" [colors]="seriesColor" [legend]="false"></app-bars-chart>
</div>
</div>
}
<div class="d-flex">
<div class="flex-fill py-2">
<mat-divider class="mt-3 mb-0"></mat-divider>
<app-history-table [headers]="seriesHeader" [names]="seriesName" [values]="seriesValue" [rows]="seriesRow" [colors]="seriesColorClass"></app-history-table>
</div>
</div>
</mat-card-content>
</mat-card>
</div>
@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { AeronefsComponent } from './aeronefs.component';
describe('AeronefsComponent', () => {
let component: AeronefsComponent;
let fixture: ComponentFixture<AeronefsComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [AeronefsComponent]
})
.compileComponents();
fixture = TestBed.createComponent(AeronefsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,113 @@
import { DatePipe } from '@angular/common';
import { trigger, state, style, animate, transition } from '@angular/animations';
import { Component, DestroyRef, inject, OnInit, OnDestroy } from '@angular/core';
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { ActivatedRoute } from '@angular/router';
import { MatBadgeModule } from '@angular/material/badge';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatDividerModule } from '@angular/material/divider';
import { MatExpansionModule } from '@angular/material/expansion';
import { MatGridListModule } from '@angular/material/grid-list';
import { MatIconModule } from '@angular/material/icon';
import { MatMenuModule } from '@angular/material/menu';
import { Observable, Subscription } from 'rxjs';
import { MenuItems } from '@components/shared';
import { HistoryTableComponent } from '@components/shared/helpers-jump';
import { BarsChartComponent, PieChartComponent } from '@components/shared/helpers-chart';
import { Jump, AeronefsPageData } from '@models';
import { UtilitiesService } from '@services';
import { AeronefByImat, AeronefByYear } from '@models';
@Component({
selector: 'app-aeronefs',
standalone: true,
imports: [
DatePipe,
MatBadgeModule, MatButtonModule, MatCardModule, MatDividerModule,
MatExpansionModule, MatGridListModule, MatIconModule, MatMenuModule,
BarsChartComponent, HistoryTableComponent, PieChartComponent
],
templateUrl: './aeronefs.component.html',
styleUrl: './aeronefs.component.scss',
animations: [
trigger('flyInOut', [
state('in', style({ transform: 'translateX(0)' })),
transition('void => *', [
style({ transform: 'translateX(-100%)' }),
animate(200)
]),
transition('* => void', [
style({ transform: 'translateX(100%)' }),
animate(200)
])
])
]
})
export class AeronefsComponent implements OnInit, OnDestroy {
private _data: Subscription = new Subscription();
private _aeronefsByImat!: Array<AeronefByImat>;
private _aeronefsByYear!: Array<AeronefByYear>;
public lastJump: Jump = {} as Jump;
public title = 'Aéronefs';
public subtitle: string = 'Nombre total de sauts par aéronef';
public displayCharts = false;
public destroyRef = inject(DestroyRef);
public seriesHeader: string[] = [];
public seriesName: string[] = [];
public seriesValue: number[] = [];
public seriesRow: Array<Array<number>> = [];
public seriesColor: {
backgroundColor: string[],
borderColor: string[]
} = {
backgroundColor: this._utilitiesService.getSeriesColors(0.8, 'all'),
borderColor: this._utilitiesService.getSeriesColors(1, 'all')
};
public seriesColorClass: string[] = this._utilitiesService.getChartColors();
constructor(
private route: ActivatedRoute,
private _utilitiesService: UtilitiesService,
public menuItems: MenuItems
) { }
ngOnInit() {
const data$: Observable<{ aeronefsPageData: AeronefsPageData }> = this.route.data as Observable<{ aeronefsPageData: AeronefsPageData }>;
this._data = data$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((data: { aeronefsPageData: AeronefsPageData }) => {
const pageData: AeronefsPageData = data.aeronefsPageData;
this.lastJump = pageData.lastjump;
this._loadAeronefByImat(pageData);
});
}
ngOnDestroy() {
this._data.unsubscribe();
}
private _loadAeronefByImat(pageData: AeronefsPageData): void {
this._aeronefsByImat = pageData.aeronefsByImat.map((row: AeronefByImat) => {
this.seriesName.push(`${row.aeronef} ${row.imat}`);
this.seriesValue.push(row.count);
return row;
});
this._loadAeronefByYear(pageData);
}
private _loadAeronefByYear(pageData: AeronefsPageData): void {
this.seriesHeader = ['2018', '2019', '2020', '2021', '2022', '2023', '2024'];
const values: Array<number> = this.seriesHeader.map(() => 0);
this.seriesName.forEach(() => {
this.seriesRow.push([...values]);
});
this._aeronefsByYear = pageData.aeronefsByImatByYear.map((row: AeronefByYear) => {
const aeronef: string = row.aeronef;
const imat: string = row.imat;
const year: string = row.year.toString();
this.seriesRow[this.seriesName.indexOf(`${aeronef} ${imat}`)][this.seriesHeader.indexOf(year)] = row.count;
return row;
});
this.displayCharts = true;
}
}
+113
View File
@@ -0,0 +1,113 @@
<div class="container auth-page">
<div class="row">
<div class="col-lg-8 offset-lg-2 col-xl-6 offset-xl-3 col-xs-12">
<h1 class="text-xs-center">{{ title }}</h1>
<p class="text-xs-center">
@if (authType === 'register') {
<a [routerLink]="['/login']">Vous avez déjà un compte ?</a>
}
@if (authType === 'login') {
<a [routerLink]="['/register']">Vous n'avez pas encore de compte ?</a>
}
</p>
<app-list-errors [errors]="errors"></app-list-errors>
<form [formGroup]="authForm" (ngSubmit)="submitForm()">
<fieldset [disabled]="isSubmitting" class="w-100">
<legend [hidden]="true">Informations personnelles</legend>
<div class="col-12">
<mat-form-field appearance="fill" class="w-100">
<input matInput type="text" placeholder="Votre adresse email" formControlName="email" required>
@if (this.authForm.controls['email'].status === 'INVALID') {
<mat-error>{{getErrorMessage('email')}}</mat-error>
}
</mat-form-field>
</div>
@if (authType === 'register') {
<div class="col-12">
<mat-form-field appearance="fill" class="w-100">
<input matInput type="text" placeholder="Votre nom" formControlName="lastname" required>
@if (this.authForm.controls['lastname'].status === 'INVALID') {
<mat-error>{{getErrorMessage('lastname')}}</mat-error>
}
</mat-form-field>
</div>
}
@if (authType === 'register') {
<div class="col-12">
<mat-form-field appearance="fill" class="w-100">
<input matInput type="text" placeholder="Votre prénom" formControlName="firstname" required>
@if (this.authForm.controls['firstname'].status === 'INVALID') {
<mat-error>{{getErrorMessage('firstname')}}</mat-error>
}
</mat-form-field>
</div>
}
@if (authType === 'register') {
<div class="col-12">
<mat-form-field appearance="fill" class="w-100">
<input matInput type="text" placeholder="Numéro de téléphone" formControlName="phone" required>
@if (this.authForm.controls['phone'].status === 'INVALID') {
<mat-error>{{getErrorMessage('phone')}}</mat-error>
}
</mat-form-field>
</div>
}
@if (authType === 'register') {
<div class="col-12">
<mat-form-field appearance="fill" class="w-100">
<input matInput type="text" placeholder="Pseudo" formControlName="username">
@if (this.authForm.controls['username'].status === 'INVALID') {
<mat-error>{{getErrorMessage('username')}}</mat-error>
}
</mat-form-field>
</div>
}
@if (authType === 'register') {
<div class="col-12">
<mat-form-field appearance="fill" class="w-100">
<input matInput type="text" placeholder="Numéro de licence FFP" formControlName="licence">
@if (this.authForm.controls['licence'].status === 'INVALID') {
<mat-error>{{getErrorMessage('licence')}}</mat-error>
}
</mat-form-field>
</div>
}
@if (authType === 'register') {
<div class="col-12">
<mat-form-field appearance="fill" class="w-100">
<input matInput type="text" placeholder="Indiquez le poids en Kg" formControlName="poids">
@if (this.authForm.controls['poids'].status === 'INVALID') {
<mat-error>{{getErrorMessage('poids')}}</mat-error>
}
</mat-form-field>
</div>
}
</fieldset>
<fieldset [disabled]="isSubmitting" class="w-100">
<legend [hidden]="true">Informations de connexion</legend>
<div class="col-12">
<mat-form-field appearance="fill" class="w-100">
<input matInput type="password" placeholder="Mot de passe" formControlName="password" required>
@if (this.authForm.controls['password'].status === 'INVALID') {
<mat-error>{{getErrorMessage('password')}}</mat-error>
}
</mat-form-field>
</div>
@if (authType === 'register') {
<div class="col-12">
<mat-form-field appearance="fill" class="w-100">
<input matInput type="password" placeholder="Confirmez votre nouveau mot de passe" formControlName="confirmPassword" required>
@if (this.authForm.controls['confirmPassword'].status === 'INVALID') {
<mat-error>{{getErrorMessage('confirmPassword')}}</mat-error>
}
</mat-form-field>
</div>
}
<div class="col-12">
<button mat-raised-button color="primary" [disabled]="!authForm.valid" type="submit" class="float-sm-end">{{ btnTitle }}</button>
</div>
</fieldset>
</form>
</div>
</div>
</div>
@@ -0,0 +1,6 @@
fieldset {
border-style: none;
border-width: 0;
margin: 0;
padding: 0;
}
@@ -0,0 +1,21 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { AuthComponent } from './auth.component';
describe('AuthComponent', () => {
let component: AuthComponent;
let fixture: ComponentFixture<AuthComponent>;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [AuthComponent]
});
fixture = TestBed.createComponent(AuthComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
+168
View File
@@ -0,0 +1,168 @@
import { Component, DestroyRef, inject, OnInit, OnDestroy } from '@angular/core';
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { UntypedFormBuilder, UntypedFormGroup, AbstractControl, Validators, ValidatorFn, ValidationErrors, FormsModule, ReactiveFormsModule } from '@angular/forms';
import { Title } from '@angular/platform-browser';
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
import { MatButtonModule } from '@angular/material/button';
import { MatInputModule } from '@angular/material/input';
import { MatFormFieldModule } from '@angular/material/form-field';
import { Subscription } from 'rxjs';
import { take } from 'rxjs/operators';
import { Errors } from '@models';
import { UserService } from '@services';
import { ListErrorsComponent, ShowAuthedDirective } from '@components/shared';
@Component({
standalone: true,
imports: [
RouterLink,
FormsModule,
ReactiveFormsModule,
MatFormFieldModule,
MatInputModule,
MatButtonModule,
ListErrorsComponent,
ShowAuthedDirective
],
selector: 'app-auth',
templateUrl: './auth.component.html',
styleUrl: './auth.component.scss'
})
export class AuthComponent implements OnInit, OnDestroy {
private _url: Subscription = new Subscription();
private _user: Subscription = new Subscription();
authType = '';
title = '';
btnTitle = '';
errors: Errors = { errors: {} };
isSubmitting = false;
authForm!: UntypedFormGroup;
destroyRef = inject(DestroyRef);
constructor(
private route: ActivatedRoute,
private router: Router,
private titleService: Title,
private userService: UserService,
private fb: UntypedFormBuilder
) {
this._resetErrors();
// use FormBuilder to create a form group
const controlsConfig = {
email: ['', [Validators.required, Validators.email]],
username: '',
firstname: '',
lastname: '',
phone: '',
licence: '',
poids: '',
password: ['', Validators.required],
confirmPassword: ''
};
this.authForm = this.fb.group(controlsConfig, { validators: this.checkPasswords });
}
ngOnInit() {
// you would normally unsubscribe from this observable subscription
// the active route observables are exemptions from unsubribe always rule
// see notes on: https://angular.io/guide/router#observable-parammap-and-component-reuse
this.route.url.pipe(take(1))
.subscribe(data => {
// Get the last piece of the URL (it's either 'login' or 'register')
this.authType = data[data.length - 1].path;
// Set a title for the page accordingly
if (this.authType === 'register') {
this.title = 'Création de compte';
this.btnTitle = 'Créer le compte';
this.titleService.setTitle(`Ad Astra - Créer un compte`);
} else {
this.title = 'Se connecter';
this.btnTitle = 'Se connecter';
this.titleService.setTitle(`Ad Astra - Connexion`);
}
// add form control for username if this is the register page
if (this.authType === 'register') {
this.authForm.controls['firstname'].setValidators([Validators.required]);
this.authForm.controls['lastname'].setValidators([Validators.required]);
this.authForm.controls['phone'].setValidators([Validators.required]);
this.authForm.controls['confirmPassword'].setValidators([Validators.minLength(8), Validators.required]);
}
});
}
ngOnDestroy() {
this._user.unsubscribe();
//this._url.unsubscribe();
}
submitForm() {
this.isSubmitting = true;
this.errors = { errors: {} };
if (this.authForm.valid) {
this._resetErrors();
const credentials = this.authForm.value;
const user$ = this.userService.attemptAuth(this.authType, credentials).pipe(takeUntilDestroyed(this.destroyRef));
this._user = user$.subscribe({
next: () => void this.router.navigateByUrl('/'),
error: (err) => {
console.log(err);
this.errors = err;
this.isSubmitting = false;
}
});
}
}
getErrorMessage(name: string) {
switch (name) {
case 'email':
if (this.authForm.controls['email'].errors !== null) {
return 'Indiquez votre adresse email';
}
break;
case 'firstname':
if (this.authForm.controls['firstname'].errors !== null) {
return 'Vous devez indiquer votre prénnom';
}
break;
case 'lastname':
if (this.authForm.controls['lastname'].errors !== null) {
return 'Vous devez indiquer votre nom';
}
break;
case 'phone':
if (this.authForm.controls['phone'].errors !== null) {
return 'Vous devez indiquer votre numéro de téléphone';
}
break;
case 'password':
if (this.authForm.controls['password'].errors !== null) {
return 'Un mot de passe est obligatoire';
}
break;
case 'confirmPassword':
if (this.authForm.controls['confirmPassword'].errors !== null) {
return 'Une confirmation du mot de passe est obligatoire';
}
break;
}
return '';
}
checkPasswords: ValidatorFn = (group: AbstractControl): ValidationErrors | null => {
if (this.authType !== 'register') {
return null;
}
const pass = group.get('password')!.value;
const confirmPass = group.get('confirmPassword')!.value
return (pass === confirmPass && pass !== '') ? null : { notSame: true }
}
private _resetErrors(): void {
this.errors = { errors: {} };
}
}
@@ -0,0 +1,257 @@
<div @flyInOut>
<div class="d-flex">
<div class="flex-fill">
<h1 class="mb-1">{{title}}</h1>
<span class="me-2 fs-6"><span class="fs-5 mx-1">DT48</span> - 08 février 2024</span>
</div>
<span class="flex-spacer"></span>
<div class="align-self-center me-2">
<button mat-icon-button [matMenuTriggerFor]="menuCalculator" aria-label="Menu Calculator">
<mat-icon>more_vert</mat-icon>
</button>
<mat-menu #menuCalculator="matMenu">
@for (menuitem of menuItems.getMenuCalculator(); track menuitem) {
@if (menuitem.type === 'link') {
<button mat-menu-item [routerLink]="['/', menuitem.state]">
<mat-icon [fontIcon]="menuitem.icon"></mat-icon>
<span>{{ menuitem.name }}</span>
</button>
}
}
</mat-menu>
</div>
</div>
<mat-divider class="my-3"></mat-divider>
<mat-card class="calculator">
<mat-card-header class="bg-warn rounded-top pt-1 pb-2 px-2">
<form #searchFormData="ngForm" class="inputs-form">
<mat-form-field class="bg-crystal me-3" subscriptSizing="dynamic" color="accent">
<mat-label>Nombre de sauts</mat-label>
<input matInput [(ngModel)]="inputs.jumps" (change)="refresh()" type="number" min="0" max="2000"
step="1" name="jumps">
</mat-form-field>
<mat-form-field class="bg-crystal me-3" subscriptSizing="dynamic" color="accent">
<mat-label>Poids nu</mat-label>
<input matInput [(ngModel)]="inputs.weight" (change)="refresh()" type="number" min="30"
max="110" name="weight">
</mat-form-field>
<mat-form-field class="bg-crystal me-3" subscriptSizing="dynamic" color="accent">
<mat-label>Poids équipement</mat-label>
<input matInput [(ngModel)]="inputs.gear" (change)="refresh()" type="number" min="0"
name="gear">
</mat-form-field>
<mat-form-field class="bg-crystal me-3" subscriptSizing="dynamic" color="accent">
<mat-label>Taille actuelle</mat-label>
<input matInput [(ngModel)]="inputs.current" (change)="refresh()" type="number" min="0"
name="current" id="current" placeholder="Taille actuelle">
</mat-form-field>
</form>
<span class="flex-spacer"></span>
<div class="flex-fill"></div>
</mat-card-header>
<mat-card-content class="my-4">
<div class="row">
<div class="col-lg-6 col-md-8 col-sm-12">
<table class="table table-striped table-dark table-hover mat-elevation-z2">
<thead>
<tr class="warn">
<th class="text-nowrap pe-4"></th>
<th class="text-nowrap">Taille actuelle</th>
<th class="text-nowrap">Taille min</th>
<th class="text-nowrap">Taille min -11%</th>
</tr>
</thead>
<tbody>
<tr class="warn">
<th class="text-nowrap pe-4">Taille de voile en ft<sup>2</sup></th>
<td class="font-monospace text-{{ info.state.color }}">
{{ info.sizesFeet.current }} </td>
<td class="font-monospace text-success"> ● {{ info.sizesFeet.min }} </td>
<td class="font-monospace text-warning"> ● {{ info.sizesFeet.min11 }} </td>
</tr>
<tr class="warn">
<th class="text-nowrap pe-4">Charge alaire</th>
<td class="font-monospace">{{ info.charges.current | number : '1.2-3' }}</td>
<td class="font-monospace">{{ info.charges.min | number : '1.2-3' }}</td>
<td class="font-monospace">{{ info.charges.min11 | number : '1.2-3' }}</td>
</tr>
<tr class="warn">
<th class="text-nowrap pe-4">Taille de voile en m<sup>2</sup></th>
<td class="font-monospace">{{ info.sizesMeter.current | number : '1.2-3' }}</td>
<td class="font-monospace">{{ info.sizesMeter.min | number : '1.2-3' }}</td>
<td class="font-monospace">{{ info.sizesMeter.min11 | number : '1.2-3' }}</td>
</tr>
<tr class="warn">
<th class="text-nowrap pe-4">Coefficient kilo/livres</th>
<td colspan="3" class="font-monospace text-start">{{ coeffKgLbs }}</td>
</tr>
<tr class="warn">
<th class="text-nowrap pe-4">Coefficient pied/mètre</th>
<td colspan="3" class="font-monospace text-start">{{ coeffFtM }}</td>
</tr>
<tr class="warn">
<th class="text-nowrap pe-4">Formule charge alaire</th>
<td colspan="3" class="font-monospace text-start">
( ( {{ inputs.weight }} + {{ inputs.gear }} ) * {{ coeffKgLbs }} ) /
{{ inputs.current }} = {{ info.charges.current | number : '1.5' }}<br />
( ( poids nu en kg + poids equipement en kg ) * coefficient kg/lbs ) / taille de
voile en ft<sup>2</sup>
</td>
</tr>
</tbody>
</table>
</div>
<div class="col-lg-6 col-md-8 col-sm-12">
@if (displayCharts) {
<div class="linechart position-relative w-100 mt-3">
<canvas baseChart class="mx-auto" width="800" height="280"
[data]="chartConfig.lineChartData" [options]="chartConfig.lineChartOptions"
[legend]="chartConfig.lineChartLegend" [type]="'line'">
</canvas>
</div>
}
</div>
</div>
<mat-divider class="my-3"></mat-divider>
<h4 class="py-2 mb-0">Tailles de voile pour {{ inputs.weight }} kg</h4>
<div>
Prochain changement de valeurs dans {{ ((this.currentRange.end + 1) - inputs.jumps) }}
{{ ((this.currentRange.end + 1) - inputs.jumps) > 1 ? 'sauts' : 'saut'}}<br />
</div>
<div class="text-end">
{{ ((((inputs.jumps - this.currentRange.start) + 1) / ((this.currentRange.end - this.currentRange.start) + 1)) * 100) | number : '1.2-2' }}%
de la tranche actuelle ({{this.currentRange.start}} à {{this.currentRange.end}})
</div>
<mat-progress-bar mode="determinate"
[value]="((((inputs.jumps - this.currentRange.start) + 1) / ((this.currentRange.end - this.currentRange.start) + 1)) * 100)"
color="warn"></mat-progress-bar>
<table class="table table-striped table-dark table-hover mat-elevation-z2 mb-1">
<thead>
<tr class="warn">
<th class="text-nowrap pe-4"></th>
@for (header of sizesHeader; track header; let index = $index) {
<th class="pl-3 pr-3 {{ tableHeader[(index+1)].active }}">{{ header.name }}</th>
}
</tr>
</thead>
<tbody>
<tr class="warn">
<th class="firtext-nowrap pe-4st">Taille de voile en ft<sup>2</sup></th>
@for (value of sizesValuesFeet; track value; let index = $index) {
<td class="pl-3 text-nowrap {{ tableHeader[(index+1)].active }}">{{ value }}</td>
}
</tr>
<tr class="warn">
<th class="text-nowrap pe-4">Abbattement 11%</th>
@for (value of sizesMinValuesFeet; track value; let index = $index) {
<td class="pl-3 text-nowrap {{ tableHeader[(index+1)].active }}">{{ value }}</td>
}
</tr>
<tr class="warn">
<th class="text-nowrap pe-4">Charge alaire</th>
@for (charge of sizesCharges; track charge; let index = $index) {
<td class="pl-3 text-nowrap {{ tableHeader[(index+1)].active }}">
{{ charge | number : '1.2-3' }}</td>
}
</tr>
<tr class="warn">
<th class="text-nowrap pe-4">Taille de voile en m<sup>2</sup></th>
@for (value of sizesValuesMeter; track value; let index = $index) {
<td class="pl-3 text-nowrap {{ tableHeader[(index+1)].active }}">
{{ value | number : '1.2-2' }}</td>
}
</tr>
</tbody>
</table>
<mat-divider class="my-3"></mat-divider>
<h4 class="py-2 mb-0">Tableau des tailles de voile par poids</h4>
<div class="text-end">
{{ ((inputs.jumps / 1600) * 100) | number : '1.2-2' }}% de la plage complète (0 à 1600)
</div>
<mat-progress-bar mode="determinate" [value]="((inputs.jumps / 1600) * 100)" color="warn">
</mat-progress-bar>
<table mat-table [dataSource]="canopy_size_table" matSort
class="table table-striped table-dark table-hover mat-elevation-z2">
<!-- weight column -->
<ng-container matColumnDef="weight">
<th mat-header-cell *matHeaderCellDef mat-sort-header class="text-nowrap pe-4 {{tableHeader[0].active}}">
{{tableHeader[0].name}} </th>
<th mat-cell *matCellDef="let element;" class="text-end ps-3 pe-4 fs-6" role="button"
(click)="setCurrentWeight(element.weight)">
<span class="text-warn d-block">{{element.weight}}</span>
</th>
</ng-container>
<!-- range_1 column -->
<ng-container matColumnDef="range_1">
<th class="pl-3" mat-header-cell *matHeaderCellDef class="{{tableHeader[1].active}}">
{{tableHeader[1].name}} </th>
<td mat-cell *matCellDef="let element;"
class="pl-3 text-nowrap fs-6 {{element.ranges[0].active}}">{{element.ranges[0].value}}</td>
</ng-container>
<!-- range_2 column -->
<ng-container matColumnDef="range_2">
<th class="pl-3" mat-header-cell *matHeaderCellDef class="{{tableHeader[2].active}}">
{{tableHeader[2].name}} </th>
<td mat-cell *matCellDef="let element;"
class="pl-3 text-nowrap fs-6 {{element.ranges[1].active}}">{{element.ranges[1].value}}</td>
</ng-container>
<!-- range_3 column -->
<ng-container matColumnDef="range_3">
<th class="pl-3" mat-header-cell *matHeaderCellDef class="{{tableHeader[3].active}}">
{{tableHeader[3].name}} </th>
<td mat-cell *matCellDef="let element;"
class="pl-3 text-nowrap fs-6 {{element.ranges[2].active}}">{{element.ranges[2].value}}</td>
</ng-container>
<!-- range_4 column -->
<ng-container matColumnDef="range_4">
<th class="pl-3" mat-header-cell *matHeaderCellDef class="{{tableHeader[4].active}}">
{{tableHeader[4].name}} </th>
<td mat-cell *matCellDef="let element;"
class="pl-3 text-nowrap fs-6 {{element.ranges[3].active}}">{{element.ranges[3].value}}</td>
</ng-container>
<!-- range_5 column -->
<ng-container matColumnDef="range_5">
<th class="pl-3" mat-header-cell *matHeaderCellDef class="{{tableHeader[5].active}}">
{{tableHeader[5].name}} </th>
<td mat-cell *matCellDef="let element;"
class="pl-3 text-nowrap fs-6 {{element.ranges[4].active}}">{{element.ranges[4].value}}</td>
</ng-container>
<!-- range_6 column -->
<ng-container matColumnDef="range_6">
<th class="pl-3" mat-header-cell *matHeaderCellDef class="{{tableHeader[6].active}}">
{{tableHeader[6].name}} </th>
<td mat-cell *matCellDef="let element;"
class="pl-3 text-nowrap fs-6 {{element.ranges[5].active}}">{{element.ranges[5].value}}</td>
</ng-container>
<!-- range_7 column -->
<ng-container matColumnDef="range_7">
<th class="pl-3" mat-header-cell *matHeaderCellDef class="{{tableHeader[7].active}}">
{{tableHeader[7].name}} </th>
<td mat-cell *matCellDef="let element;"
class="pl-3 text-nowrap fs-6 {{element.ranges[6].active}}">{{element.ranges[6].value}}</td>
</ng-container>
<!-- range_8 column -->
<ng-container matColumnDef="range_8">
<th class="pl-3" mat-header-cell *matHeaderCellDef class="{{tableHeader[8].active}}">
{{tableHeader[8].name}} </th>
<td mat-cell *matCellDef="let element;"
class="pl-3 text-nowrap fs-6 {{element.ranges[7].active}}">{{element.ranges[7].value}}</td>
</ng-container>
<!-- range_9 column -->
<ng-container matColumnDef="range_9">
<th class="pl-3" mat-header-cell *matHeaderCellDef class="{{tableHeader[9].active}}">
{{tableHeader[9].name}} </th>
<td mat-cell *matCellDef="let element;"
class="pl-3 text-nowrap fs-6 {{element.ranges[8].active}}">{{element.ranges[8].value}}</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns; sticky: false" class="warn"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"
class="text-middle font-monospace warn {{row.active}}" id="weight_{{row.weight}}"></tr>
</table>
<div class="clearfix">
<mat-paginator [pageSize]="50" [pageSizeOptions]="[5, 10, 15, 25, 50]" showFirstLastButtons
class="pull-right mb-1 mt-2"></mat-paginator>
</div>
</mat-card-content>
</mat-card>
</div>
@@ -0,0 +1,33 @@
.calculator-page {
section {
display: table;
}
.demo-label {
display: table-cell;
font-size: 14px;
margin-left: 8px;
min-width: 120px;
}
.demo-button-row {
display: table-cell;
max-width: 900px;
}
.demo-button-row .mat-mdc-button-base {
margin: 8px 8px 8px 0;
}
.demo-flex-container {
display: flex;
justify-content: space-between;
flex-wrap: wrap;
}
.demo-button-container {
display: flex;
justify-content: center;
width: 120px;
}
th {
&.first {
width: 180px;
}
}
}
@@ -0,0 +1,21 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { CalculatorComponent } from './calculator.component';
describe('CalculatorComponent', () => {
let component: CalculatorComponent;
let fixture: ComponentFixture<CalculatorComponent>;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [CalculatorComponent]
});
fixture = TestBed.createComponent(CalculatorComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,222 @@
import { Component, Input, OnInit, OnDestroy, ViewChild } from '@angular/core';
import { trigger, state, style, animate, transition } from '@angular/animations';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { RouterModule } from '@angular/router';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
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 { MatMenuModule } from '@angular/material/menu';
import { MatPaginator, MatPaginatorModule } from '@angular/material/paginator';
import { MatProgressBarModule } from '@angular/material/progress-bar';
import { MatSort, MatSortModule } from '@angular/material/sort';
import { MatTableDataSource, MatTableModule } from '@angular/material/table';
import { BaseChartDirective } from 'ng2-charts';
import { Observable, Subscription } from 'rxjs';
import { MenuItems } from '@components/shared';
import { CalculatorInfo, CalculatorResult, CalculatorState, InputParams, Jump, LineConfig, WeightSizeRange, TableHeader, User, WeightSize, weightSizes } from '@models';
import { CalculatorService, JumpsService, UserService, UtilitiesService } from '@services';
@Component({
selector: 'app-calculator',
standalone: true,
imports: [
CommonModule, FormsModule, RouterModule,
MatButtonModule, MatCardModule, MatDividerModule,
MatFormFieldModule, MatInputModule, MatIconModule, MatMenuModule,
MatPaginatorModule, MatProgressBarModule, MatSortModule, MatTableModule,
BaseChartDirective
],
templateUrl: './calculator.component.html',
styleUrl: './calculator.component.scss',
animations: [
trigger('flyInOut', [
state('in', style({ transform: 'translateY(0)' })),
transition('void => *', [
style({ transform: 'translateY(-100%)' }),
animate(200)
]),
transition('* => void', [
style({ transform: 'translateY(100%)' }),
animate(200)
])
])
]
})
export class CalculatorComponent implements OnInit, OnDestroy {
//private _lastjump!: Subscription; // = new Subscription();
//private _currentUser!: Subscription; // = new Subscription();
private _subscriptions: Array<Subscription> = [];
public title = 'Calcul de taille de voile';
public subtitle = 'DT48 - 08 février 2024';
public lastjump: Jump = {} as Jump;
public currentRange: WeightSizeRange = {} as WeightSizeRange;
public currentUser: User = {} as User;
public displayedColumns: Array<string> = ['weight'];
public tableHeader: Array<TableHeader> = [<TableHeader>{name: 'Poids nu en kg', active: ''}];
public sizesHeader: Array<TableHeader> = [];
public sizesValuesFeet: Array<number> = [];
public sizesMinValuesFeet: Array<number> = [];
public sizesCharges: Array<number> = [];
public sizesValuesMeter: Array<number> = [];
public displayCharts = false;
public chartConfig: LineConfig = this._utilitiesService.getLineChartConfig();
public coeffKgLbs: number = this._utilitiesService.getCoeffKgLbs();
public coeffFtM: number = this._utilitiesService.getCoeffFtM();
public info: CalculatorInfo = {
charges: <CalculatorResult>{ current: 0, min: 0, min11: 0 },
sizesFeet: <CalculatorResult>{ current: 0, min: 0, min11: 0 },
sizesMeter: <CalculatorResult>{ current: 0, min: 0, min11: 0 },
state: <CalculatorState>{ color: 'danger' }
}
@Input() canopy_size_table!: MatTableDataSource<WeightSize>;
@Input() inputs: InputParams = <InputParams>{
jumps: 0,
weight: 60,
gear: 10,
current: 190
} as InputParams;
@ViewChild(MatSort) sort!: MatSort;
@ViewChild(MatPaginator) paginator!: MatPaginator;
constructor(
private _calculatorService: CalculatorService,
private _jumpsService: JumpsService,
private _userService: UserService,
private _utilitiesService: UtilitiesService,
public menuItems: MenuItems
) { }
ngOnInit() {
const currentUser$: Observable<User> = this._userService.currentUser;
const lastjump$: Observable<Jump> = this._jumpsService.getLastJump();
weightSizes[0].ranges.forEach((range: WeightSizeRange) => {
const header: TableHeader = {name: range.label, active: range.active};
this.tableHeader.push(header)
this.sizesHeader.push(header);
this.displayedColumns.push(range.name);
});
this.chartConfig.lineChartData.labels = [...this.sizesHeader.map(
(data: TableHeader): string => data.name
)];
this.chartConfig.lineChartData.datasets = [
{
label: 'Tailles min -11%',
data: this.sizesMinValuesFeet,
fill: true,
stepped: true,
pointStyle: false,
borderColor: 'rgba(241, 80, 80, 1)',
backgroundColor: 'rgba(241, 80, 80, 0.3)'
},
{
label: 'Tailles min',
data: this.sizesValuesFeet,
fill: true,
stepped: true,
pointStyle: false,
borderColor: 'rgba(32, 182, 252, 1)',
backgroundColor: 'rgba(32, 182, 252, 0.3)'
}
];
this._subscriptions.push(
currentUser$.subscribe((userData: User) => {
this.currentUser = userData;
this._subscriptions.push(
lastjump$.subscribe((jump) => {
this.lastjump = jump;
this.inputs.jumps = jump.numero;
this.inputs.weight = this.currentUser.poids!;
this.inputs.current = jump.taille!;
this.canopy_size_table = new MatTableDataSource<WeightSize>(weightSizes);
this.refresh();
})
);
})
);
}
ngOnDestroy() {
this._subscriptions.forEach((sub: Subscription) => {
sub.unsubscribe();
});
}
public refresh():void {
this._refreshActive();
this._refreshSizes();
}
private _refreshActive(): void {
let weight: number = this.inputs.weight;
if (this.inputs.weight < 60) {
weight = 60;
}
weightSizes.forEach(element => {
element.active = '';
element.ranges.forEach(range => {
range.active = '';
});
});
this.tableHeader.forEach(element => {
element.active = '';
});
if (weight >= 60 && weight <= 110) {
weightSizes[(weight - 60)].active = 'active';
}
if (this.inputs.jumps >= 0) {
const num = this._calculatorService.getRangeNum(this.inputs.jumps);
this.tableHeader[num].active = 'active';
weightSizes.forEach(element => {
element.ranges[(num-1)].active = 'active';
});
this.currentRange = weightSizes[0].ranges[(num-1)];
}
}
private _refreshSizes(): void {
let weight: number = this.inputs.weight;
if (this.inputs.weight < 60) {
weight = 60;
}
if (this.inputs.jumps > 2000) {
this.info.state.error = '2K+ Jumps';
}
this.sizesValuesFeet = this._calculatorService.getCanopySizes(weight);
this.sizesMinValuesFeet = this._calculatorService.getCanopySizes(weight, true);
this.sizesValuesMeter = this.sizesValuesFeet.map((value: number, index: number) => {
this.sizesCharges[index] = this._calculatorService.getCharge(value, this.inputs.weight, this.inputs.gear);
return this._calculatorService.convertFeet2Meters(value);
});
this.info.sizesFeet = this._calculatorService.canopySizeCalc(weight, this.inputs.jumps);
this.info.sizesFeet.current = this.inputs.current;
this.info.sizesMeter = {
current: this._calculatorService.convertFeet2Meters(this.inputs.current),
min: this._calculatorService.convertFeet2Meters(this.info.sizesFeet.min),
min11: this._calculatorService.convertFeet2Meters(this.info.sizesFeet.min11)
}
this.info.charges = {
current: this._calculatorService.getCharge(this.inputs.current, this.inputs.weight, this.inputs.gear),
min: this._calculatorService.getCharge(this.info.sizesFeet.min, this.inputs.weight, this.inputs.gear),
min11: this._calculatorService.getCharge(this.info.sizesFeet.min11, this.inputs.weight, this.inputs.gear)
}
this.info.state.color = this._calculatorService.getStateColor(this.info.sizesFeet);
this.chartConfig.lineChartData.datasets[0].data = this.sizesMinValuesFeet;
this.chartConfig.lineChartData.datasets[1].data = this.sizesValuesFeet;
this.displayCharts = true;
}
public setCurrentWeight(weight: number) {
this.inputs.weight = weight;
this.refresh();
}
}
@@ -0,0 +1,56 @@
<div @flyInOut>
<div class="d-flex">
<div class="flex-fill">
<h1 class="mb-1">{{title}}</h1>
<span [hidden]="!lastJump.numero" class="me-2 fs-6">
Dernier saut enregistré : <span class="fs-5 mx-1 text-primary">{{ lastJump.numero }}<sup>ème</sup></span> à <span class="fs-5 mx-1 text-primary">{{ lastJump.lieu }}</span> le <span class="fs-5 mx-1 text-primary">{{ lastJump.date | date: 'dd/MM/yyyy' }}</span>
</span>
</div>
<span class="flex-spacer"></span>
<div class="align-self-center me-2">
<button mat-icon-button [matMenuTriggerFor]="menuCanopyModel" aria-label="Menu Canopy Model">
<mat-icon fontIcon="more_vert"></mat-icon>
</button>
<mat-menu #menuCanopyModel="matMenu">
@for (menuitem of menuItems.getMenuPanel(); track menuitem) {
<button mat-menu-item>
<mat-icon [fontIcon]="menuitem.icon"></mat-icon>
<span>{{ menuitem.name }}</span>
</button>
}
</mat-menu>
</div>
</div>
<mat-divider class="my-3"></mat-divider>
<mat-card>
<mat-card-header>
<mat-card-title>{{ subtitle }}</mat-card-title>
</mat-card-header>
<mat-card-content class="mt-4">
@if (displayCharts) {
<div class="row">
<div class="col-xl-3 col-lg-6 col-md-6 col-sm-12">
<app-pie-chart [headers]="seriesModelHeader" [names]="seriesModelName" [values]="seriesModelValue" [colors]="seriesColor"></app-pie-chart>
</div>
<div class="col-xl-3 col-lg-6 col-md-6 col-sm-12">
<app-pie-chart [headers]="seriesSizeHeader" [names]="seriesSizeName" [values]="seriesSizeValue" [colors]="seriesColor"></app-pie-chart>
</div>
<div class="col-xl-6 col-lg-12 col-md-12">
<!--<app-pie-chart [headers]="seriesSizeHeader" [names]="seriesSizeName" [values]="seriesSizeValue" [colors]="seriesColor"></app-pie-chart>-->
<app-bars-chart [headers]="seriesSizeHeader" [names]="seriesSizeName" [values]="seriesSizeRow" [colors]="seriesColor" [legend]="false"></app-bars-chart>
</div>
</div>
}
<div class="row">
<div class="col-xl-6 col-lg-12">
<mat-divider class="mt-3 mb-0"></mat-divider>
<app-history-table [headers]="seriesModelHeader" [names]="seriesModelName" [values]="seriesModelValue" [rows]="seriesModelRow" [colors]="seriesColorClass"></app-history-table>
</div>
<div class="col-xl-6 col-lg-12">
<mat-divider class="mt-3 mb-0"></mat-divider>
<app-history-table [headers]="seriesSizeHeader" [names]="seriesSizeName" [values]="seriesSizeValue" [rows]="seriesSizeRow" [colors]="seriesColorClass"></app-history-table>
</div>
</div>
</mat-card-content>
</mat-card>
</div>
@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { CanopiesComponent } from './canopies.component';
describe('CanopiesComponent', () => {
let component: CanopiesComponent;
let fixture: ComponentFixture<CanopiesComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [CanopiesComponent]
})
.compileComponents();
fixture = TestBed.createComponent(CanopiesComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,142 @@
import { DatePipe } from '@angular/common';
import { trigger, state, style, animate, transition } from '@angular/animations';
import { Component, DestroyRef, inject, OnInit, OnDestroy } from '@angular/core';
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { ActivatedRoute } from '@angular/router';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatDividerModule } from '@angular/material/divider';
import { MatExpansionModule } from '@angular/material/expansion';
import { MatGridListModule } from '@angular/material/grid-list';
import { MatIconModule } from '@angular/material/icon';
import { MatMenuModule } from '@angular/material/menu';
import { Observable, Subscription } from 'rxjs';
import { MenuItems } from '@components/shared';
import { HistoryTableComponent } from '@components/shared/helpers-jump';
import { BarsChartComponent, PieChartComponent } from '@components/shared/helpers-chart';
import { Jump, CanopiesPageData } from '@models';
import { UtilitiesService } from '@services';
import { CanopyBySize, CanopyByYear, CanopyModelBySize, CanopyModelByYear } from '@models';
@Component({
selector: 'app-canopies',
standalone: true,
imports: [
DatePipe,
MatButtonModule, MatCardModule, MatDividerModule, MatExpansionModule,
MatGridListModule, MatIconModule, MatMenuModule,
HistoryTableComponent, BarsChartComponent, PieChartComponent
],
templateUrl: './canopies.component.html',
styleUrl: './canopies.component.scss',
animations: [
trigger('flyInOut', [
state('in', style({ transform: 'translateX(0)' })),
transition('void => *', [
style({ transform: 'translateX(-100%)' }),
animate(200)
]),
transition('* => void', [
style({ transform: 'translateX(100%)' }),
animate(200)
])
])
]
})
export class CanopiesComponent implements OnInit, OnDestroy {
private _data: Subscription = new Subscription();
private _canopiesModelBySize!: Array<CanopyModelBySize>;
private _canopiesModelByYear!: Array<CanopyModelByYear>;
private _canopiesSizeBySize!: Array<CanopyBySize>;
private _canopiesSizeByYear!: Array<CanopyByYear>;
public lastJump: Jump = {} as Jump;
public title = 'Voiles';
public subtitle: string = 'Nombre total de sauts par modèle et taille';
public displayCharts = false;
public destroyRef = inject(DestroyRef);
public seriesModelHeader: string[] = [];
public seriesModelName: string[] = [];
public seriesModelValue: number[] = [];
public seriesModelRow: Array<Array<number>> = [];
public seriesSizeHeader: string[] = [];
public seriesSizeName: string[] = [];
public seriesSizeValue: number[] = [];
public seriesSizeRow: Array<Array<number>> = [];
public seriesColor: {
backgroundColor: string[],
borderColor: string[]
} = {
backgroundColor: this._utilitiesService.getSeriesColors(0.8, 'all'),
borderColor: this._utilitiesService.getSeriesColors(1, 'all')
};
public seriesColorClass: string[] = this._utilitiesService.getChartColors();
constructor(
private route: ActivatedRoute,
private _utilitiesService: UtilitiesService,
public menuItems: MenuItems
) { }
ngOnInit() {
const data$: Observable<{ canopiesPageData: CanopiesPageData }> = this.route.data as Observable<{ canopiesPageData: CanopiesPageData }>;
this._data = data$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((data: { canopiesPageData: CanopiesPageData }) => {
const pageData: CanopiesPageData = data.canopiesPageData;
this.lastJump = pageData.lastjump;
this._loadCanopyBySize(pageData);
this._loadCanopyModelBySize(pageData);
});
}
ngOnDestroy() {
this._data.unsubscribe();
}
private _loadCanopyBySize(pageData: CanopiesPageData): void {
this._canopiesSizeBySize = pageData.canopiesBySize.map((row: CanopyBySize) => {
this.seriesSizeName.push(row.taille.toString());
this.seriesSizeValue.push(row.count);
return row;
});
this._loadCanopyByYear(pageData);
}
private _loadCanopyByYear(pageData: CanopiesPageData): void {
this.seriesSizeHeader = ['2018', '2019', '2020', '2021', '2022', '2023', '2024'];
const values: Array<number> = this.seriesSizeHeader.map(() => 0);
this.seriesSizeName.forEach(() => {
this.seriesSizeRow.push([...values]);
});
this._canopiesSizeByYear = pageData.canopiesBySizeByYear.map((row: CanopyByYear) => {
const taille: string = row.taille.toString();
const year: string = row.year.toString();
this.seriesSizeRow[this.seriesSizeName.indexOf(taille)][this.seriesSizeHeader.indexOf(year)] = row.count;
return row;
});
}
private _loadCanopyModelBySize(pageData: CanopiesPageData): void {
this._canopiesModelBySize = pageData.canopiesBySizeByModel.map((row: CanopyModelBySize) => {
this.seriesModelName.push(`${row.taille.toString()} - ${row.voile}`);
this.seriesModelValue.push(row.count);
return row;
});
this._loadCanopyModelByYear(pageData);
}
private _loadCanopyModelByYear(pageData: CanopiesPageData): void {
this.seriesModelHeader = ['2018', '2019', '2020', '2021', '2022', '2023', '2024'];
const values: Array<number> = this.seriesModelHeader.map(() => 0);
this.seriesModelName.forEach(() => {
this.seriesModelRow.push([...values]);
});
this._canopiesModelByYear = pageData.canopiesBySizeByModelByYear.map((row: CanopyModelByYear) => {
const voile: string = row.voile;
const taille: string = row.taille.toString();
const year: string = row.year.toString();
this.seriesModelRow[this.seriesModelName.indexOf(`${taille} - ${voile}`)][this.seriesModelHeader.indexOf(year)] = row.count;
return row;
});
this.displayCharts = true;
}
}
@@ -0,0 +1,36 @@
<h1 class="mb-1">{{title}}</h1>
<span [hidden]="!lastJump.numero" class="me-2 fst-italic0 fs-6">
Dernier saut enregistré : <span class="fs-5 mx-1 text-primary">{{ lastJump.numero }}<sup>ème</sup></span> à <span class="fs-5 mx-1 text-primary">{{ lastJump.lieu }}</span> le <span class="fs-5 mx-1 text-primary">{{ lastJump.date | date: 'dd/MM/yyyy' }}</span>
</span>
<mat-divider class="my-3"></mat-divider>
<ng-container *appShowAuthed="true">
<div class="row">
<div class="col-xs-12">
<app-jumps-by-month></app-jumps-by-month>
</div>
</div>
<div class="row mt-sm-2 mt-lg-3">
<div class="col-md-4 col-sm-12">
<app-dropzones-pie></app-dropzones-pie>
</div>
<div class="col-md-8 col-sm-12">
<app-dropzones-bar></app-dropzones-bar>
</div>
</div>
<div class="row mt-sm-2 mt-lg-3">
<div class="col-md-4 col-sm-12">
<app-aeronefs-pie></app-aeronefs-pie>
</div>
<div class="col-md-8 col-sm-12">
<app-aeronefs-bar></app-aeronefs-bar>
</div>
</div>
<div class="row mt-sm-2 mt-lg-3">
<div class="col-md-6 col-sm-12">
<app-canopies-models></app-canopies-models>
</div>
<div class="col-md-6 col-sm-12">
<app-canopies-sizes></app-canopies-sizes>
</div>
</div>
</ng-container>
@@ -0,0 +1,21 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { DashboardComponent } from './dashboard.component';
describe('DashboardComponent', () => {
let component: DashboardComponent;
let fixture: ComponentFixture<DashboardComponent>;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [DashboardComponent]
});
fixture = TestBed.createComponent(DashboardComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,147 @@
import { Component, OnInit, OnDestroy } from '@angular/core';
import { DatePipe } from '@angular/common';
import { Title } from '@angular/platform-browser';
import { MatDividerModule } from '@angular/material/divider';
import { ActivatedRoute, Data, Router } from '@angular/router';
import { Observable, Subscription } from 'rxjs';
import { take } from 'rxjs/operators';
import { Aeronef, AeronefByImat, Canopy, CanopyModelBySize, DropZone, DropZoneByYear, Errors, Jump, User } from '@models';
//import { AeronefsService, CanopiesService, DropZonesService, UserService } from '@services';
import { JumpsService, UserService } from '@services';
import { ShowAuthedDirective } from '@components/shared/show-authed.directive';
import {
AeronefsPieComponent, AeronefsBarComponent,
CanopiesModelsComponent, CanopiesSizesComponent,
DropzonesPieComponent, DropzonesBarComponent,
JumpsByMonthComponent
} from '@components/shared/dashboard-components';
@Component({
standalone: true,
imports: [
DatePipe, ShowAuthedDirective, MatDividerModule,
AeronefsPieComponent, AeronefsBarComponent,
CanopiesModelsComponent, CanopiesSizesComponent,
DropzonesPieComponent, DropzonesBarComponent,
JumpsByMonthComponent
],
selector: 'app-dashboard',
templateUrl: './dashboard.component.html',
styleUrl: './dashboard.component.scss'
})
export class DashboardComponent implements OnInit, OnDestroy {
private _currentUser: Subscription = new Subscription();
private _data: Subscription = new Subscription();
private _lastjump: Subscription = new Subscription();
private _lastjump$: Observable<Jump> = new Observable();
/*
private _aeronefs: Subscription = new Subscription();
private _canopies: Subscription = new Subscription();
private _dropzones: Subscription = new Subscription();
*/
public title = 'Dashboard';
public errors!: Errors;
public isAuthenticated = false;
public canModify = false;
public aeronefAggregate!: Array<AeronefByImat>;
public aeronefs!: Array<Aeronef>;
public aeronefsCount = 0;
public canopyAggregate!: Array<CanopyModelBySize>;
public canopies!: Array<Canopy>;
public canopiesCount = 0;
public dropzoneAggregate!: Array<DropZoneByYear>;
public dropzones!: Array<DropZone>;
public dropzonesCount = 0;
public lastJump: Jump = {} as Jump;
constructor(
private route: ActivatedRoute,
private router: Router,
private titleService: Title,
private _userService: UserService,
private _jumpsService: JumpsService
/*
private aeronefsService: AeronefsService,
private canopiesService: CanopiesService,
private dropzonesService: DropZonesService
*/
) {
this._resetErrors();
}
ngOnInit() {
this.titleService.setTitle(this.title);
const data$: Observable<Data> = this.route.data.pipe(take(1));
this._data = data$.subscribe((data: Data) => {
this.isAuthenticated = data['isAuthenticated'];
if (!this.isAuthenticated) {
this.router.navigateByUrl('/login');
return;
}
const currentUser$: Observable<User> = this._userService.currentUser;
this._currentUser = currentUser$.subscribe((userData: User) => {
this.canModify = userData.role === 'Admin';
});
this._lastjump$ = this._jumpsService.getLastJump();
this._lastjump = this._lastjump$.subscribe((jump) => {
this.lastJump = jump;
});
});
/*
this._loadAeronefs();
this._loadCanopies();
this._loadDropzones();
*/
}
ngOnDestroy() {
this._currentUser.unsubscribe();
this._data.unsubscribe();
this._lastjump.unsubscribe();
/*
this._aeronefs.unsubscribe();
this._canopies.unsubscribe();
this._dropzones.unsubscribe();
*/
}
private _resetErrors(): void {
this.errors = { errors: {} };
}
/*
private _loadAeronefs(): void {
const aeronefs$: Observable<Array<AeronefByImat>> = this.aeronefsService.getAllByImat();
this._aeronefs = aeronefs$.subscribe((aggregate: Array<AeronefByImat>) => {
this.aeronefAggregate = aggregate;
this.aeronefsCount = aggregate.length;
console.log(this.aeronefAggregate);
console.log(this.aeronefsCount);
});
}
private _loadCanopies(): void {
const canopies$: Observable<Array<CanopyModelBySize>> = this.canopiesService.getAllModelBySize();
this._canopies = canopies$.subscribe((aggregate: Array<CanopyModelBySize>) => {
this.canopyAggregate = aggregate;
this.canopiesCount = aggregate.length;
console.log(this.canopyAggregate);
console.log(this.canopiesCount);
});
}
private _loadDropzones(): void {
const dropzones$: Observable<Array<DropZoneByYear>> = this.dropzonesService.getAllByDate();
this._dropzones = dropzones$.subscribe((aggregate: Array<DropZoneByYear>) => {
this.dropzoneAggregate = aggregate;
this.dropzonesCount = aggregate.length;
console.log(this.dropzoneAggregate);
console.log(this.dropzonesCount);
});
}
*/
}
+295
View File
@@ -0,0 +1,295 @@
<div class="banner mat-elevation-z2">
<div class="banner-content d-flex text-bg-accent bg-opacity-50">
<img [src]="user.image" class="user-img me-3" [alt]="user.username" />
<div class="flex-spacer">
<h3 class="mb-0">{{bannerTitle}}</h3>
<span class="fs-6">{{user.firstname}} {{user.lastname}}</span>
</div>
<div class="align-self-center text-end">
<button mat-raised-button color="warn" routerLink="/settings">
<mat-icon aria-label="Modifier le compte" fontIcon="settings"></mat-icon> {{ btnSettingsTitle }}
</button>
</div>
</div>
</div>
<div class="demo-page">
<div class="d-flex">
<div class="flex-fill">
<h1 class="mb-1">{{title}}</h1>
</div>
<span class="flex-spacer"></span>
<div class="align-self-center me-2 text-end"></div>
</div>
<mat-divider class="my-3"></mat-divider>
<app-list-errors [errors]="errors"></app-list-errors>
</div>
<div class="content mt-3">
<mat-card>
<mat-card-header class="d-flex text-bg-primary rounded-top py-2 px-3">
<div class="flex-fill">
<mat-card-title>
Buttons variants
</mat-card-title>
<mat-card-subtitle class="fw-normal text-white">
{{ getColors().length }} colors
</mat-card-subtitle>
</div>
<span class="flex-spacer"></span>
<div class="align-self-center">
<button mat-icon-button>
<mat-icon>more_vert</mat-icon>
</button>
</div>
</mat-card-header>
<mat-card-content>
<section class="mt-4">
<div class="mb-2">Links</div>
<div class="d-flex flex-wrap align-items-start">
@for (color of getColors(); track color) {
<div class="m-2">
<a href="#" class="text-{{ color }}">{{ color }}</a>
</div>
}
</div>
<div class="d-flex flex-wrap align-items-start">
@for (color of getColors(); track color) {
<div class="m-2">
<span class="text-{{ color }}">{{ color }}</span>
</div>
}
</div>
</section>
<mat-divider class="my-4"></mat-divider>
<section>
<div class="mb-2">Normal</div>
<div class="d-flex flex-wrap align-items-start">
@for (color of getColors(); track color) {
<div class="m-2">
<button mat-button class="text-nowrap" [color]="color">{{ color }}</button>
</div>
}
<div class="m-2">
<button mat-button>Basic</button>
</div>
<div class="m-2">
<button mat-button disabled>Disabled</button>
</div>
<div class="m-2">
<a mat-button href="https://www.google.com/" target="_blank">&lt;a Link</a>
</div>
</div>
</section>
<mat-divider class="my-4"></mat-divider>
<section>
<div class="mb-2">Stroked</div>
<div class="d-flex flex-wrap align-items-start">
@for (color of getColors(); track color) {
<div class="m-2">
<button mat-stroked-button class="text-nowrap" [color]="color">{{ color }}</button>
</div>
}
<div class="m-2">
<button mat-stroked-button>Basic</button>
</div>
<div class="m-2">
<button mat-stroked-button disabled>Disabled</button>
</div>
<div class="m-2">
<a mat-stroked-button href="https://www.google.com/" target="_blank">&lt;a Link</a>
</div>
</div>
</section>
<mat-divider class="my-4"></mat-divider>
<section>
<div class="mb-2">Flat</div>
<div class="d-flex flex-wrap align-items-start">
@for (color of getColors(); track color) {
<div class="m-2">
<button mat-flat-button class="text-nowrap" [color]="color">{{ color }}</button>
</div>
}
<div class="m-2">
<button mat-flat-button>Basic</button>
</div>
<div class="m-2">
<button mat-flat-button disabled>Disabled</button>
</div>
<div class="m-2">
<a mat-flat-button href="https://www.google.com/" target="_blank">&lt;a Link</a>
</div>
</div>
</section>
<mat-divider class="my-4"></mat-divider>
<section>
<div class="mb-2">Raised</div>
<div class="d-flex flex-wrap align-items-start">
@for (color of getColors(); track color) {
<div class="m-2">
<button mat-raised-button class="text-nowrap" [color]="color">{{ color }}</button>
</div>
}
<div class="m-2">
<button mat-raised-button>Basic</button>
</div>
<div class="m-2">
<button mat-raised-button disabled>Disabled</button>
</div>
<div class="m-2">
<a mat-raised-button href="https://www.google.com/" target="_blank">&lt;a Link</a>
</div>
</div>
</section>
<mat-divider class="my-4"></mat-divider>
<section>
<div class="mb-2">FAB</div>
<div class="d-flex flex-wrap align-items-start">
@for (color of getColors(); track color; let index = $index) {
<div class="m-2">
<button mat-fab [color]="color" aria-label="Example icon button with an icon">
<mat-icon>{{ getIcon(index) }}</mat-icon>
</button>
</div>
}
<div class="m-2">
<button mat-fab color="white" aria-label="Example icon button with a vertical three dot icon">
<mat-icon>more_vert</mat-icon>
</button>
</div>
<div class="m-2">
<button mat-fab disabled aria-label="Example icon button with a block icon">
<mat-icon>block</mat-icon>
</button>
</div>
</div>
</section>
<mat-divider class="my-4"></mat-divider>
<section>
<div class="mb-2">FAB extended</div>
<div class="d-flex flex-wrap align-items-start">
@for (color of getColors(); track color; let index = $index) {
<div class="m-2">
<button mat-fab extended [color]="color" aria-label="Example icon button with an icon">
<mat-icon>{{ getIcon(index) }}</mat-icon>
{{ color }}
</button>
</div>
}
<div class="m-2">
<button mat-fab extended color="white" aria-label="Example icon button with a vertical three dot icon">
<mat-icon>more_vert</mat-icon>
white
</button>
</div>
<div class="m-2">
<button mat-fab extended disabled aria-label="Example icon button with a block icon">
<mat-icon>block</mat-icon>
disabled
</button>
</div>
</div>
</section>
<mat-divider class="my-4"></mat-divider>
<section>
<div class="mb-2">Mini FAB</div>
<div class="d-flex flex-wrap align-items-start">
@for (color of getColors(); track color; let index = $index) {
<div class="m-2">
<button mat-mini-fab [color]="color" aria-label="Example icon button with an icon">
<mat-icon>{{ getIcon(index) }}</mat-icon>
</button>
</div>
}
<div class="m-2">
<button mat-mini-fab color="white" aria-label="Example icon button with a vertical three dot icon">
<mat-icon>more_vert</mat-icon>
</button>
</div>
<div class="m-2">
<button mat-mini-fab disabled aria-label="Example icon button with a block icon">
<mat-icon>block</mat-icon>
</button>
</div>
</div>
</section>
<mat-divider class="my-4"></mat-divider>
<section>
<div class="mb-2">Icon</div>
<div class="d-flex flex-wrap align-items-start">
@for (color of getColors(); track color; let index = $index) {
<div class="m-2">
<button mat-icon-button [color]="color" aria-label="Example icon button with an icon">
<mat-icon>{{ getIcon(index) }}</mat-icon>
</button>
</div>
}
<div class="m-2">
<button mat-icon-button aria-label="Example icon button with a vertical three dot icon">
<mat-icon>more_vert</mat-icon>
</button>
</div>
<div class="m-2">
<button mat-icon-button disabled aria-label="Example icon button with a open in new tab icon">
<mat-icon>open_in_new</mat-icon>
</button>
</div>
</div>
</section>
</mat-card-content>
</mat-card>
</div>
<mat-divider class="my-4"></mat-divider>
<div cdkDropList class="row mb-3" (cdkDropListDropped)="drop($event)">
@for (color of getColors(); track color) {
<div class="col-xs-12 col-lg-6 col-xxl-4 pb-3" cdkDrag>
<div class="card-custom-placeholder col-xs-12 col-lg-6 col-xxl-4 pb-3" *cdkDragPlaceholder></div>
<mat-card>
<mat-card-header class="d-flex text-bg-{{ color }} rounded-top py-2 px-3">
<div class="flex-fill">
<mat-card-title>
{{ color }}
</mat-card-title>
<mat-card-subtitle class="fw-normal">
Card subtitle
</mat-card-subtitle>
</div>
<span class="flex-spacer"></span>
<div class="align-self-center">
<button mat-icon-button>
<mat-icon>more_vert</mat-icon>
</button>
</div>
</mat-card-header>
<mat-card-content>
<section class="mt-4 mb-3">
<div class="d-flex flex-wrap align-items-start">
<button mat-raised-button [color]="color" class="m-2 align-self-center">Texte Raised</button>
<button mat-flat-button [color]="color" class="m-2 align-self-center">Texte Flat</button>
<button mat-stroked-button [color]="color" class="m-2 align-self-center">Texte Stroked</button>
<button mat-button [color]="color" class="m-2 align-self-center">{{ color }}</button>
</div>
<div class="d-flex flex-wrap align-items-start mb-3">
<button mat-fab extended [color]="color" class="m-2 align-self-center">
<mat-icon>check_circle</mat-icon>
{{ color }}
</button>
<button mat-fab [color]="color" class="m-2 align-self-center">
<mat-icon>check_circle</mat-icon>
</button>
<button mat-mini-fab [color]="color" class="m-2 align-self-center">
<mat-icon>check_circle</mat-icon>
</button>
<button mat-icon-button [color]="color" class="m-2 align-self-center">
<mat-icon>check_circle</mat-icon>
</button>
</div>
<mat-progress-bar [color]="color" mode="determinate" value="40"></mat-progress-bar>
<mat-slider class="w-100" min="0" max="1000">
<input value="300" matSliderStartThumb>
<input value="600" matSliderEndThumb>
</mat-slider>
</section>
</mat-card-content>
</mat-card>
</div>
}
</div>
@@ -0,0 +1,58 @@
/* Demo */
@use 'variable' as var;
.mat-mdc-card + .mat-mdc-card {
margin-top: 10px;
}
.card-list {
width: 500px;
max-width: 100%;
border: solid 1px #ccc;
min-height: 60px;
display: block;
background: var.$white;
border-radius: 4px;
overflow: hidden;
}
.card-box {
padding: 20px 10px;
border-bottom: solid 1px #ccc;
color: rgba(0, 0, 0, 0.87);
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
box-sizing: border-box;
cursor: move;
background: var.$white;
font-size: 14px;
}
.cdk-drag-preview {
border: none;
box-sizing: border-box;
border-radius: 4px;
box-shadow: 0 5px 5px -3px rgba(0, 0, 0, 0.2),
0 8px 10px 1px rgba(0, 0, 0, 0.14),
0 3px 14px 2px rgba(0, 0, 0, 0.12);
}
.cdk-drag-animating {
transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
}
.card-box:last-child {
border: none;
}
.card-list.cdk-drop-list-dragging .card-box:not(.cdk-drag-placeholder) {
transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
}
.card-custom-placeholder {
background: #ccc;
border: dotted 3px #999;
min-height: 60px;
transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
}
@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { DemoComponent } from './demo.component';
describe('DemoComponent', () => {
let component: DemoComponent;
let fixture: ComponentFixture<DemoComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [DemoComponent]
})
.compileComponents();
fixture = TestBed.createComponent(DemoComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
+83
View File
@@ -0,0 +1,83 @@
import { Component, OnInit, OnDestroy } from '@angular/core';
import {CdkDragDrop, CdkDropList, CdkDrag, CdkDragPlaceholder, moveItemInArray} from '@angular/cdk/drag-drop';
import { Title } from '@angular/platform-browser';
import { RouterLink } from '@angular/router';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatDividerModule } from '@angular/material/divider';
import { MatIconModule } from '@angular/material/icon';
import { MatProgressBarModule } from '@angular/material/progress-bar';
import { MatSliderModule } from '@angular/material/slider';
import { Subscription } from 'rxjs';
import { Errors, User } from '@models';
import { UserService } from '@services';
import { ListErrorsComponent } from '@components/shared';
@Component({
selector: 'app-demo',
standalone: true,
imports: [
CdkDropList, CdkDrag, CdkDragPlaceholder, RouterLink,
MatButtonModule, MatCardModule, MatDividerModule,
MatIconModule, MatProgressBarModule, MatSliderModule,
ListErrorsComponent
],
templateUrl: './demo.component.html',
styleUrl: './demo.component.scss'
})
export class DemoComponent implements OnInit, OnDestroy {
private _user: Subscription = new Subscription();
private _colors: string[] = [
"primary", "accent", "warn", "secondary", "megna", "raspberry", "success", "info",
"warning", "danger", "red", "orange", "yellow", "green", "teal", "turquoise", "cyan",
"blue", "navy", "navy-light", "pink", "magenta", "purple", "purple-light", "grey", "muted"
];
private _icons: string[] = [
'mode_edit', 'delete', 'bookmark', 'settings', 'home', 'person',
'share', 'account_circle', 'public', 'search', 'favorite', 'done',
'numbers', 'attach_file', 'insert_comment', 'format_list_bulleted',
'filter', 'crop', 'edit_attributes', 'upload', 'download', 'expand_less',
'expand_more', 'chevron_left', 'chevron_right', 'sync', 'power',
'power_off', 'notifications', 'emoji_emotions', 'person_add', 'block'
];
title = 'Demo';
bannerTitle = '';
user: Partial<User> = {} as Partial<User>;
errors: Errors = { errors: {} };
btnSettingsTitle = 'Paramètres';
constructor(
private titleService: Title,
private userService: UserService
) { }
ngOnInit() {
this.titleService.setTitle(`Ad Astra - ${this.title}`);
Object.assign(this.user, this.userService.getCurrentUser());
if (this.user.username) {
this.bannerTitle = `@${this.user.username}`;
} else {
this.bannerTitle = `${this.user.firstname} ${this.user.lastname}`;
}
}
ngOnDestroy() {
this._user.unsubscribe();
}
getColors(): string[] {
return this._colors;
}
getIcon(index: number): string {
return this._icons[index];
}
drop(event: CdkDragDrop<string[]>) {
moveItemInArray(this._colors, event.previousIndex, event.currentIndex);
}
}
@@ -0,0 +1,68 @@
<div @flyInOut>
<div class="d-flex">
<div class="flex-fill">
<h1 class="mb-1">{{title}}</h1>
<span [hidden]="!lastJump.numero" class="me-2 fs-6">
Dernier saut enregistré : <span class="fs-5 mx-1 text-primary">{{ lastJump.numero }}<sup>ème</sup></span> à <span class="fs-5 mx-1 text-primary">{{ lastJump.lieu }}</span> le <span class="fs-5 mx-1 text-primary">{{ lastJump.date | date: 'dd/MM/yyyy' }}</span>
</span>
</div>
<span class="flex-spacer"></span>
<div class="align-self-center me-2">
<button mat-icon-button [matMenuTriggerFor]="menuDropzone" aria-label="Menu Dropzone">
<mat-icon fontIcon="more_vert"></mat-icon>
</button>
<mat-menu #menuDropzone="matMenu">
@for (menuitem of menuItems.getMenuPanel(); track menuitem) {
<button mat-menu-item>
<mat-icon [fontIcon]="menuitem.icon"></mat-icon>
<span>{{ menuitem.name }}</span>
</button>
}
</mat-menu>
</div>
</div>
<mat-divider class="my-3"></mat-divider>
<mat-card>
<mat-card-header>
<mat-card-title>{{ subtitle }}</mat-card-title>
</mat-card-header>
<mat-card-content class="mt-4">
@if (displayCharts) {
<div class="row">
<div class="col-xl-4 col-lg-5 col-md-6 col-sm-12">
<app-pie-chart [headers]="seriesHeader" [names]="seriesName" [values]="seriesValue" [colors]="seriesColor"></app-pie-chart>
</div>
<div class="col-xl-8 col-lg-7 col-md-6 col-sm-12">
<app-bars-chart [headers]="seriesHeader" [names]="seriesName" [values]="seriesRow" [colors]="seriesColor" [legend]="false"></app-bars-chart>
<!--
@if (displayCharts) {
<div class="barchart position-relative w-100 my-1">
<x-chartist [configuration]="chartBarConfig"></x-chartist>
</div>
}
-->
</div>
</div>
}
<div class="d-flex">
<div class="flex-fill py-2">
<mat-divider class="mt-3 mb-0"></mat-divider>
<app-history-table [headers]="seriesHeader" [names]="seriesName" [values]="seriesValue" [rows]="seriesRow" [colors]="seriesColorClass"></app-history-table>
</div>
</div>
</mat-card-content>
</mat-card>
<mat-divider class="my-3"></mat-divider>
<mat-card>
<mat-card-header>
<mat-card-title>Carte des DZ</mat-card-title>
</mat-card-header>
<mat-card-content class="mt-4">
<div class="ratio ratio-21x9">
<google-map height="100%" width="100%" [center]="center" [zoom]="zoom" [options]="options">
<map-kml-layer [url]="kmlUrl" />
</google-map>
</div>
</mat-card-content>
</mat-card>
</div>
@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { DropzonesComponent } from './dropzones.component';
describe('DropzonesComponent', () => {
let component: DropzonesComponent;
let fixture: ComponentFixture<DropzonesComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [DropzonesComponent]
})
.compileComponents();
fixture = TestBed.createComponent(DropzonesComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,188 @@
import { DatePipe } from '@angular/common';
import { trigger, state, style, animate, transition } from '@angular/animations';
import { Component, DestroyRef, inject, OnInit, OnDestroy } from '@angular/core';
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { GoogleMap, MapKmlLayer} from '@angular/google-maps';
import { ActivatedRoute } from '@angular/router';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatDividerModule } from '@angular/material/divider';
import { MatExpansionModule } from '@angular/material/expansion';
import { MatGridListModule } from '@angular/material/grid-list';
import { MatIconModule } from '@angular/material/icon';
import { MatMenuModule } from '@angular/material/menu';
//import { ChartistModule, Configuration } from 'ng-chartist';
//import { AxisOptions, Label } from 'chartist';
import { Observable, Subscription } from 'rxjs';
import { MenuItems } from '@components/shared';
import { BarsChartComponent, PieChartComponent } from '@components/shared/helpers-chart';
import { HistoryTableComponent } from '@components/shared/helpers-jump';
import { DropZoneByOaci, DropZoneByYear, DropZonesPageData, Jump } from '@models';
import { UtilitiesService } from '@services';
@Component({
selector: 'app-dropzones',
standalone: true,
imports: [
DatePipe, GoogleMap, MapKmlLayer,
MatButtonModule, MatCardModule, MatDividerModule, MatExpansionModule,
MatGridListModule, MatIconModule, MatMenuModule,
//ChartistModule,
HistoryTableComponent, BarsChartComponent, PieChartComponent
],
templateUrl: './dropzones.component.html',
styleUrl: './dropzones.component.scss',
animations: [
trigger('flyInOut', [
state('in', style({ transform: 'translateX(0)' })),
transition('void => *', [
style({ transform: 'translateX(-100%)' }),
animate(200)
]),
transition('* => void', [
style({ transform: 'translateX(100%)' }),
animate(200)
])
])
]
})
export class DropzonesComponent implements OnInit, OnDestroy {
private _data: Subscription = new Subscription();
private _lastjump: Subscription = new Subscription();
private _dropzonesByOaci!: Array<DropZoneByOaci>;
private _dropzonesByYear!: Array<DropZoneByYear>;
public lastJump: Jump = {} as Jump;
public title = 'Drop zones';
public subtitle: string = 'Nombre total de sauts par drop zone';
public displayCharts = false;
public destroyRef = inject(DestroyRef);
public seriesHeader: string[] = [];
public seriesName: string[] = [];
public seriesValue: number[] = [];
public seriesRow: Array<Array<number>> = [];
public seriesColor: {
backgroundColor: string[],
borderColor: string[]
} = {
backgroundColor: this._utilitiesService.getSeriesColors(0.8, 'all'),
borderColor: this._utilitiesService.getSeriesColors(1, 'all')
};
public seriesColorClass: string[] = this._utilitiesService.getChartColors();
//public chartBarConfig: Configuration = this._utilitiesService.getBarConfig();
public center: google.maps.LatLngLiteral = {lat: 44.596408, lng: -1.115958};
public zoom = 16;
public kmlUrl = 'https://rampeur.com/assets/kmls/dropzones.kml?v=6';
//public mapTypeId: google.maps.MapTypeId = google.maps.MapTypeId.SATELLITE;
public options: google.maps.MapOptions = {
//zoomControl: false,
//scrollwheel: false,
//mapTypeId: google.maps.MapTypeId.SATELLITE,
mapTypeId: 'satellite',
disableDoubleClickZoom: true,
maxZoom: 20,
minZoom: 3
};
// Arcachon : 44.596408,-1.115958
// La Réole : 44.566309,-0.054606
// Soulac : 45.495395,-1.081039
// Royan : 45.632660,-0.977096
// Rochefort : 45.888477,-0.984551
// Sables : 46.475393,-1.722160
// Pamiers : 43.091951,1.698546
// Pau : 43.426255,-0.288453
// Cahors : 44.348635,1.478898
// Béni : 32.394218,-6.327999
constructor(
private route: ActivatedRoute,
private _utilitiesService: UtilitiesService,
public menuItems: MenuItems
) { }
ngOnInit() {
const data$: Observable<{ dropZonesPageData: DropZonesPageData }> = this.route.data as Observable<{ dropZonesPageData: DropZonesPageData }>;
this._data = data$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((data: { dropZonesPageData: DropZonesPageData }) => {
const pageData: DropZonesPageData = data.dropZonesPageData;
this.lastJump = pageData.lastjump;
this._loadDropZoneByOaci(pageData);
});
}
ngOnDestroy() {
this._data.unsubscribe();
}
private _loadDropZoneByOaci(pageData: DropZonesPageData): void {
this._dropzonesByOaci = pageData.dropZonesByOaci.map((row: DropZoneByOaci) => {
this.seriesName.push(`${row.oaci} - ${row.lieu}`);
this.seriesValue.push(row.count);
return row;
});
this._loadDropZoneByYear(pageData);
}
private _loadDropZoneByYear(pageData: DropZonesPageData): void {
this.seriesHeader = ['2018', '2019', '2020', '2021', '2022', '2023', '2024'];
const values: Array<number> = this.seriesHeader.map(() => 0);
this.seriesName.forEach(() => {
this.seriesRow.push([...values]);
});
this._dropzonesByYear = pageData.dropZonesByOaciByYear.map((row: DropZoneByYear) => {
const lieu: string = row.lieu;
const oaci: string = row.oaci;
const year: string = row.year.toString();
this.seriesRow[this.seriesName.indexOf(`${oaci} - ${lieu}`)][this.seriesHeader.indexOf(year)] = row.count;
return row;
});
/*
const axisX: AxisOptions = {
labelInterpolationFnc: function(value: Label, index: number): string {
return index % 1 === 0 ? `${value.toString()}` : '';
}
}
this.chartBarConfig = {
type: 'Bar',
data: {
'labels': this.seriesHeader,
'series': this.seriesRow
},
options: {
seriesBarDistance: 15,
horizontalBars: false,
high: 180,
axisX: {
showGrid: false,
offset: 20
},
axisY: {
showGrid: true,
offset: 40
},
height: 300
},
responsiveOptions: [
['screen and (min-width: 1024px)', {
axisX: axisX
}],
['screen and (min-width: 641px) and (max-width: 1024px)', {
seriesBarDistance: 7,
axisY: {
offset: 30
},
axisX: axisX
}],
['screen and (max-width: 640px)', {
seriesBarDistance: 5,
axisY: {
offset: 20
},
axisX: axisX
}]
]
};
*/
this.displayCharts = true;
}
}
@@ -0,0 +1,114 @@
<div @flyInOut>
<div class="d-flex">
<div class="flex-fill">
<h1 class="mb-1">{{title}}</h1>
</div>
<span class="flex-spacer"></span>
<div class="align-self-center me-2 text-end">
<span class="fs-5 text-primary">{{ now | date: 'dd/MM/yyyy' }}</span>
</div>
</div>
<mat-divider class="my-3"></mat-divider>
<app-list-errors [errors]="errors"></app-list-errors>
<div class="d-flex">
<p class="fs-3">{{description}}</p>
</div>
<mat-divider class="mt-2 mb-3"></mat-divider>
<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">
<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>
</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>
<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>
</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>
<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>
</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>
<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="{{ minValue }}">
<mat-icon matSuffix>percent</mat-icon>
<mat-hint>excess power in %</mat-hint>
</mat-form-field>
</div>
</div>
<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>
</div>
@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { HerowarsGuildraidComponent } from './herowars-guildraid.component';
describe('HerowarsGuildraidComponent', () => {
let component: HerowarsGuildraidComponent;
let fixture: ComponentFixture<HerowarsGuildraidComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [HerowarsGuildraidComponent]
})
.compileComponents();
fixture = TestBed.createComponent(HerowarsGuildraidComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,94 @@
import { DatePipe } from '@angular/common';
import { Title } from '@angular/platform-browser';
import { trigger, state, style, animate, transition } from '@angular/animations';
import { Component, DestroyRef, Injectable, inject, OnInit } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { STEPPER_GLOBAL_OPTIONS } from '@angular/cdk/stepper';
import { MatCardModule } from '@angular/material/card';
import { MatButtonModule } from '@angular/material/button';
import { MatDividerModule } from '@angular/material/divider';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { MatStepperIntl, MatStepperModule } from '@angular/material/stepper';
import { ListErrorsComponent } from '@components/shared';
import { Errors } from '@models';
import { HWGuildRaid, HWGuildRaidAttacker, HWGuildRaidAttackers, HWGuildRaidStage, HWMember } from '@models/herowars';
import guildRaids from 'src/files-data/hw-guild-raids.json'; // page Asgard -> Guild Raid -> Log
@Injectable()
export class GuildRaidStepperIntl extends MatStepperIntl {
// the default optional label text, if unspecified is "Optional"
override optionalLabel = 'Optional Label';
}
@Component({
selector: 'app-herowars-guildraid',
standalone: true,
providers: [
{
provide: STEPPER_GLOBAL_OPTIONS,
useValue: { displayDefaultIndicatorType: false },
},
],
imports: [
DatePipe, FormsModule, ListErrorsComponent,
MatButtonModule, MatCardModule, MatDividerModule, MatFormFieldModule,
MatIconModule, MatInputModule, MatStepperModule
],
templateUrl: './herowars-guildraid.component.html',
styleUrl: './herowars-guildraid.component.scss',
animations: [
trigger('flyInOut', [
state('in', style({ transform: 'translateY(0)' })),
transition('void => *', [
style({ transform: 'translateY(-100%)' }),
animate(200)
]),
transition('* => void', [
style({ transform: 'translateY(100%)' }),
animate(200)
])
])
]
})
export class HerowarsGuildraidComponent implements OnInit {
public title = 'Guild Raids';
public subtitle = 'Log';
public description = 'Tools in working progress.';
public errors: Errors = { errors: {} };
public destroyRef = inject(DestroyRef);
public minVarPct = 3;
public minValue = 0;
public now = new Date();
private _guildMembers: HWMember[] = [];
private _matStepperIntl = inject(MatStepperIntl);
private _guildRaids: HWGuildRaid[] = [];
private _raidStages: HWGuildRaidStage[] = [
{ num: 1, name: 'Stage 1', maxPower: 320000, duration: 8, startTime: 0, endTime: 0 },
{ num: 2, name: 'Stage 2', maxPower: 500000, duration: 8, startTime: 0, endTime: 0 },
{ num: 3, name: 'Stage 3', maxPower: 0, duration: 0, startTime: 0, endTime: 0 }
];
constructor(
private _titleService: Title
) { }
ngOnInit() {
this._titleService.setTitle(this.title);
this._guildMembers.map((data) => {
data.raids = [];
data.raidsInfo = {
variationAvg: 0,
variationSum: 0
};
return data;
});
}
getMembers(): HWMember[] {
return this._guildMembers;
}
}
@@ -0,0 +1,215 @@
<div @flyInOut>
<div class="d-flex">
<div class="flex-fill">
<h1 class="mb-1">{{title}}</h1>
</div>
<span class="flex-spacer"></span>
<div class="align-self-center me-2 text-end">
<span class="fs-5 text-primary">{{ now | date: 'dd/MM/yyyy' }}</span>
</div>
</div>
<mat-divider class="my-3"></mat-divider>
<app-list-errors [errors]="errors"></app-list-errors>
<div class="d-flex">
<p class="fs-3">{{description}}</p>
</div>
<mat-divider class="mt-2 mb-3"></mat-divider>
<mat-tab-group [selectedIndex]="0" color="primary">
<mat-tab label="Guild">
<div class="row g-0">
<div class="col-12 pe-md-2 border-1 border-top">
<!--<app-guildwar-attack [members]="getMembers()"></app-guildwar-attack>-->
@if (clanLoaded) {
<app-card-container [colors]="getColors('raspberry')" [title]="getClanViewModel().getTitle()" subtitle="Members: {{ getClanViewModel().getMembersCount() }}/{{ maxClanMembers }}">
<!--<app-guildwar-defence [members]="getMembers()" type="heroes"></app-guildwar-defence>-->
<div class="d-flex mt-3">
<div class="flex-fill">
<h2>
<span class="border-1 border-bottom">{{ getClanViewModel().getTitle() }} <span class="fs-4"> Server {{ getClanViewModel().getServer() }}</span></span>
</h2>
</div>
<span class="flex-spacer"></span>
<div class="align-self-center me-2 text-end">
<span class="fs-4">Members: {{ getClanViewModel().getMembersCount() }}/{{ maxClanMembers }}</span>
</div>
</div>
<div class="row g-0 my-3">
<div class="col-2">
<div class="d-flex align-items-start flex-column me-1" role="group" aria-label="Vertical button group">
<button mat-stroked-button color="warn" (click)="syncClan()" [disabled]="!clanLoaded" class="mb-1">Sync clan</button>
<button mat-stroked-button color="warn" (click)="syncMembers()" [disabled]="!membersLoaded" class="mb-1">Sync members</button>
</div>
</div>
<div class="col-5">
<dl class="dl-horizontal m-0">
<dt>country</dt>
<dd>{{ getClanViewModel().getCountry() }}</dd>
<dt>serverId</dt>
<dd>{{ getClanViewModel().getServer() }}</dd>
<dt>description</dt>
<dd>{{ getClanViewModel().getDescription() }}</dd>
<dt>level</dt>
<dd>{{ getClanViewModel().getLevel() }}</dd>
<dt>minLevel</dt>
<dd>{{ getClanViewModel().getMinLevel() }}</dd>
</dl>
</div>
<div class="col-5">
<dl class="dl-horizontal m-0">
<dt>topActivity</dt>
<dd>{{ getClanViewModel().getTopActivity() | number : '1.0' }}</dd>
<dt>topDungeon</dt>
<dd>{{ getClanViewModel().getTopDungeon() | number : '1.0' }}</dd>
<dt>disbanding</dt>
<dd>{{ getClanViewModel().getDisbanding() }}</dd>
<dt>frameId</dt>
<dd>{{ getClanViewModel().getFrameId() }}</dd>
<dt>ownerId</dt>
<dd>{{ getClanViewModel().getOwnerId() | number : '1.0' }}</dd>
</dl>
</div>
</div>
</app-card-container>
}
@else {
<div class="d-flex mt-3">
<button mat-stroked-button color="warn" (click)="loadData()" class="mb-1">Load Data</button>
</div>
}
@if (membersLoaded) {
<app-card-container [colors]="getColors('raspberry')" title="Members" subtitle="List of guild members">
<div class="table-responsive p-0">
<table class="table table-striped table-condensed border-top border-light">
<thead>
<tr role="row" class="text-bg-raspberry bg-opacity-60">
<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 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">
ID
</div>
</th>
<th scope="col" class="align-middle w-auto">
<div class="text-start">
Last Login
</div>
</th>
<th scope="col" class="align-middle w-auto">
<div class="text-start">
Dismissal Delay
</div>
</th>
<th scope="col" class="align-middle w-auto">
<div class="text-start">
Clan
</div>
</th>
<th scope="col" class="align-middle w-auto">
<div class="text-start">
Server
</div>
</th>
<th scope="col" class="align-middle w-auto">
<div class="text-end">
Heroes
</div>
</th>
<th scope="col" class="align-middle w-auto pe-4">
<div class="text-end">
Titans
</div>
</th>
</tr>
</thead>
<tbody>
@for (memberVM of getMembersViewModel().getMembers(); track memberVM.getId(); let index = $index) {
<tr role="row" class="align-middle">
<td class="cell text-start ps-4">
{{ (index+1) }}
</td>
<td class="cell text-start">
<div class="d-flex">
<mat-icon [fontIcon]="memberVM.isChampion() ? 'gpp_good' : 'gpp_bad'" class="me-2 icon-small {{ memberVM.isChampion() ? 'text-green' : 'text-muted' }}"></mat-icon>
<span [class]="memberVM.isChampion() ? 'text-green' : ''">{{ memberVM.getName() }}</span>
</div>
</td>
<td class="cell font-monospace text-start">
<span class="me-2">{{ memberVM.getId() }}</span> <small class="me-3">[{{ memberVM.getLevel() }}]</small>
</td>
<td class="cell font-monospace text-start">
<span class="me-3">{{ memberVM.getLastLoginTime() + '000' | date: 'dd/MM HH:mm' }}</span>
</td>
<td class="cell font-monospace text-start">
<span [class]="memberVM.getInactivity().daysLeft < daysToWarn ? 'text-red' : ''">{{ memberVM.getInactivity().dropDelay }}</span>
</td>
<td class="cell text-start">
{{ memberVM.getClanTitle() }}
</td>
<td class="cell text-start">
{{ memberVM.getServerId() }}
</td>
<td class="cell font-monospace text-end">
{{ memberVM.getMemberHeroes().power | number : '1.0' }}
</td>
<td class="cell font-monospace text-end pe-4">
{{ memberVM.getMemberTitans().power | number : '1.0' }}
</td>
</tr>
}
</tbody>
</table>
</div>
</app-card-container>
}
</div>
</div>
</mat-tab>
<mat-tab label="Defense">
<div class="row g-0">
<div class="col col-md-6 pe-md-2">
<app-card-container [colors]="getColors('accent')" title="Heroes Fortifications" subtitle="Defense overview">
<!--<app-guildwar-defence [members]="getMembers()" type="heroes"></app-guildwar-defence>-->
</app-card-container>
</div>
<div class="col col-md-6 ps-md-2">
<app-card-container [colors]="getColors('accent')" title="Titans Fortifications" subtitle="Defense overview">
<!--<app-guildwar-defence [members]="getMembers()" type="titans"></app-guildwar-defence>-->
</app-card-container>
</div>
</div>
</mat-tab>
<mat-tab label="Attack">
<div class="row g-0">
<div class="col col-md-6 ps-md-2">
<app-card-container [colors]="getColors('accent')" title="Heroes Fortifications" subtitle="Attack overview">
<!--<app-guildwar-attack [members]="getMembers()" type="heroes"></app-guildwar-attack>-->
</app-card-container>
</div>
<div class="col col-md-6 ps-md-2">
<app-card-container [colors]="getColors('accent')" title="Titans Fortifications" subtitle="Attack overview">
<!--<app-guildwar-attack [members]="getMembers()" type="titans"></app-guildwar-attack>-->
</app-card-container>
</div>
</div>
</mat-tab>
<mat-tab label="Raids">
<div class="row g-0">
<div class="col">
<app-card-container [colors]="getColors('megna')" title="Guild Raids" subtitle="Statistics">
<!---->
</app-card-container>
</div>
</div>
</mat-tab>
</mat-tab-group>
</div>
@@ -0,0 +1,8 @@
.btn-group-vertical {
flex-direction: column;
align-items: flex-end;
justify-content: center;
button {
margin: 2px 0;
}
}
@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { HerowarsGuildwarComponent } from './herowars-guildwar.component';
describe('HerowarsGuildwarComponent', () => {
let component: HerowarsGuildwarComponent;
let fixture: ComponentFixture<HerowarsGuildwarComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [HerowarsGuildwarComponent]
})
.compileComponents();
fixture = TestBed.createComponent(HerowarsGuildwarComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,136 @@
import { DatePipe, DecimalPipe } from '@angular/common';
import { Title } from '@angular/platform-browser';
import { trigger, state, style, animate, transition } from '@angular/animations';
import { Component, DestroyRef, inject, OnInit } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { MatCardModule } from '@angular/material/card';
import { MatButtonModule } from '@angular/material/button';
import { MatDividerModule } from '@angular/material/divider';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { MatTabsModule } from '@angular/material/tabs';
import { CardContainerComponent, ListErrorsComponent, GuildwarAttackComponent, GuildwarDefenceComponent } from '@components/shared';
import { CardColors, Errors, HWActivityStat, HWGuildClan, HWMember } from '@models';
import { ClanViewModel, MembersViewModel } from '@viewmodels/herowars';
import { HWClanService, HWMemberService, UtilitiesService } from '@services';
import { MAX_CLAN_MEMBERS } from '@constants';
import guildData from '@data/hw-guild-data.json'; // page Membres
import guildStatistics from '@data/hw-guild-statistics.json'; // page Overview -> Statistics
@Component({
selector: 'app-herowars-guildwar',
standalone: true,
imports: [
DatePipe, DecimalPipe, FormsModule, CardContainerComponent, ListErrorsComponent,
MatButtonModule, MatCardModule, MatDividerModule, MatFormFieldModule,
MatIconModule, MatInputModule, MatTabsModule,
GuildwarAttackComponent, GuildwarDefenceComponent
],
templateUrl: './herowars-guildwar.component.html',
styleUrl: './herowars-guildwar.component.scss',
animations: [
trigger('flyInOut', [
state('in', style({ transform: 'translateY(0)' })),
transition('void => *', [
style({ transform: 'translateY(-100%)' }),
animate(200)
]),
transition('* => void', [
style({ transform: 'translateY(100%)' }),
animate(200)
])
])
]
})
export class HerowarsGuildwarComponent implements OnInit {
public destroyRef = inject(DestroyRef);
public title = 'Guild War';
public subtitle = 'Log';
public description = 'Tools in working progress.';
public colors: CardColors = { background: 'bg-raspberry', text: 'text-bg-primary' };
public errors: Errors = { errors: {} };
public now = new Date();
public daysToWarn = 1;
public maxClanMembers = MAX_CLAN_MEMBERS;
public clanLoaded = false;
public membersLoaded = false;
public clanViewModel: ClanViewModel = {} as ClanViewModel;
public membersViewModel: MembersViewModel = {} as MembersViewModel;
private _guildClan: HWGuildClan = {} as HWGuildClan;
private _guildMembers: HWMember[] = [];
private _guildInfo: HWActivityStat = {} as HWActivityStat;
constructor(
private _titleService: Title,
private _utilitiesService: UtilitiesService,
private _clanService: HWClanService,
private _memberService: HWMemberService
) { }
ngOnInit() {
this._titleService.setTitle(this.title);
}
getClanViewModel(): ClanViewModel {
return this.clanViewModel;
}
getMembersViewModel(): MembersViewModel {
return this.membersViewModel;
}
getColors(color = 'megna'): CardColors {
if (color === 'accent') {
this.colors = { background: 'bg-accent', text: 'text-navy' };
} else if (color === 'raspberry') {
this.colors = { background: 'bg-raspberry', text: 'text-bg-primary' };
} else {
this.colors = { background: 'bg-megna', text: 'text-bg-primary' };
}
return this.colors;
}
loadClan(): void {
this._guildClan = this._clanService.loadClan();
this.clanViewModel = new ClanViewModel(this._guildClan);
this.clanLoaded = true;
}
loadData(): void {
this._guildClan = this._clanService.loadClan();
this._guildMembers = this._memberService.loadMembers(this._guildClan);
this._guildClan = this._clanService.loadClanStats(this._guildClan, this._guildMembers);
this.daysToWarn = (parseInt(this._guildClan.daysToKick) / 2);
this.clanViewModel = new ClanViewModel(this._guildClan);
this.clanLoaded = true;
this.membersViewModel = new MembersViewModel(this._guildMembers);
this.membersLoaded = true;
console.log(this._guildClan);
console.log(this._guildMembers);
}
loadMembers(): void {
this._guildMembers = this._memberService.loadMembers(this._guildClan);
this.membersViewModel = new MembersViewModel(this._guildMembers);
this._guildClan = this._clanService.loadClanStats(this._guildClan, this._guildMembers);
this.membersLoaded = true;
}
getMembers(): HWMember[] {
return this._guildMembers;
}
syncClan(): void {
console.log(this._guildClan);
console.log(guildData);
}
syncMembers(): void {
console.log(this._guildMembers);
console.log(guildStatistics);
}
}
@@ -0,0 +1,66 @@
<div @flyInOut>
<div class="d-flex">
<div class="flex-fill">
<h1 class="mb-1">{{title}}</h1>
</div>
<span class="flex-spacer"></span>
<div class="align-self-center me-2 text-end">
<span class="fs-5 text-primary">{{ now | date: 'dd/MM/yyyy' }}</span>
</div>
</div>
<mat-divider class="my-3"></mat-divider>
<app-list-errors [errors]="errors"></app-list-errors>
<div class="d-flex">
<p class="fs-3">{{description}}</p>
</div>
<mat-divider class="mt-2 mb-3"></mat-divider>
<mat-tab-group [selectedIndex]="3">
<mat-tab label="Guild War">
<div class="row g-0">
<div class="col col-md-6 pe-md-2">
<app-guildwar-defence [members]="getMembers()" type="heroes"></app-guildwar-defence>
</div>
<div class="col col-md-6 ps-md-2">
<app-guildwar-defence [members]="getMembers()" type="titans"></app-guildwar-defence>
</div>
</div>
<div class="row g-0">
<div class="col-12 pe-md-2">
<!--<app-guildwar-attack [members]="getMembers()"></app-guildwar-attack>-->
</div>
</div>
</mat-tab>
<mat-tab label="Champions">
<div class="row g-0">
<div class="col col-md-6 pe-md-2">
<div class="row">
<div class="col col-md-6 pe-md-2">
<app-guildwar-champions [members]="getMembers()" type="heroes"></app-guildwar-champions>
</div>
<div class="col col-md-6 ps-md-2">
<app-guildwar-champions [members]="getMembers()" type="titans"></app-guildwar-champions>
</div>
</div>
<app-guild-card></app-guild-card>
</div>
<div class="col col-md-6 ps-md-2">
<app-guildwar-teams [members]="getMembers()"></app-guildwar-teams>
</div>
</div>
</mat-tab>
<mat-tab label="Members">
<div class="row g-0">
<div class="col">
<app-members-statistics [members]="getMembers()"></app-members-statistics>
</div>
</div>
</mat-tab>
<mat-tab label="Raids">
<div class="row g-0">
<div class="col">
<app-guildraids-log [members]="getMembers()"></app-guildraids-log>
</div>
</div>
</mat-tab>
</mat-tab-group>
</div>
@@ -0,0 +1,37 @@
/* herowars */
//@use 'variable' as var;
.icon-xxsmall {
transform: scale(0.25);
}
.icon-xsmall {
font-size: 16px;
height: 16px;
width: 16px;
}
.icon-small {
font-size: 18px;
height: 18px;
width: 18px;
}
.icon-medium {
font-size: 20px;
height: 20px;
width: 20px;
}
.hstack .value {
min-width: 56px;
}
/*
#FF41F8
#F0060B
#F0070C
#CC26D5
#7702FF
#35A2EB
#B7DDD0
#B0D9CB
#D9CAAE
#9788C7
*/
@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { HerowarsComponent } from './herowars.component';
describe('HerowarsComponent', () => {
let component: HerowarsComponent;
let fixture: ComponentFixture<HerowarsComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [HerowarsComponent]
})
.compileComponents();
fixture = TestBed.createComponent(HerowarsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,84 @@
import { DatePipe } from '@angular/common';
import { Title } from '@angular/platform-browser';
import { trigger, state, style, animate, transition } from '@angular/animations';
//import { Component, DestroyRef, inject, OnInit, computed, signal, Signal } from '@angular/core';
import { Component, DestroyRef, inject, OnInit } from '@angular/core';
import { MatCardModule } from '@angular/material/card';
import { MatDividerModule } from '@angular/material/divider';
import { MatIconModule } from '@angular/material/icon';
import { MatTabsModule } from '@angular/material/tabs';
import {
ListErrorsComponent,
GuildCardComponent, GuildraidsLogComponent, GuildwarChampionsComponent,
GuildwarAttackComponent, GuildwarDefenceComponent, GuildwarTeamsComponent, MembersStatisticsComponent
} from '@components/shared';
import { Errors, HWMember } from '@models';
import guildData from 'src/files-data/hw-guild-data.json'; // page Membres
//import guildWarData from 'src/files-data/hw-guild-war.json'; // page Overview -> Statistics | page Guild War -> Guild War
//import guildStatistics from 'src/files-data/hw-guild-statistics.json'; // page Overview -> Statistics
//import guildWarSlots from 'src/files-data/hw-guild-war-slots.json'; // no pages
@Component({
selector: 'app-herowars',
standalone: true,
imports: [
DatePipe,
MatCardModule, MatDividerModule, MatIconModule, MatTabsModule,
ListErrorsComponent,
GuildCardComponent, GuildraidsLogComponent, GuildwarChampionsComponent,
GuildwarAttackComponent, GuildwarDefenceComponent, GuildwarTeamsComponent, MembersStatisticsComponent
],
templateUrl: './herowars.component.html',
styleUrl: './herowars.component.scss',
animations: [
trigger('flyInOut', [
state('in', style({ transform: 'translateY(0)' })),
transition('void => *', [
style({ transform: 'translateY(-100%)' }),
animate(200)
]),
transition('* => void', [
style({ transform: 'translateY(100%)' }),
animate(200)
])
])
]
})
export class HerowarsComponent implements OnInit {
public title = 'HeroWars GM Tools';
public description = 'Tools in working progress.';
public errors: Errors = { errors: {} };
public destroyRef = inject(DestroyRef);
public now = new Date();
private _guildMembers: HWMember[] = [];
constructor(
private _titleService: Title
) { }
ngOnInit() {
try {
this._titleService.setTitle(this.title);
for (const data of Object.entries(guildData.clan.members)) {
const member: HWMember = {} as HWMember;
Object.assign(member, data[1]);
if (guildData.clan.warriors.indexOf(parseInt(member.id)) !== -1) {
member.champion = true;
} else {
member.champion = false;
}
member.heroes = { power: 0, teams: []};
member.titans = { power: 0, teams: []};
this._guildMembers.push(member);
}
} catch (error) {
console.error(error);
}
}
getMembers(): HWMember[] {
return this._guildMembers;
}
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,37 @@
/* Home */
//@use 'variable' as var;
.icon-xxsmall {
transform: scale(0.25);
}
.icon-xsmall {
font-size: 16px;
height: 16px;
width: 16px;
}
.icon-small {
font-size: 18px;
height: 18px;
width: 18px;
}
.icon-medium {
font-size: 20px;
height: 20px;
width: 20px;
}
.hstack .value {
min-width: 56px;
}
/*
#FF41F8
#F0060B
#F0070C
#CC26D5
#7702FF
#35A2EB
#B7DDD0
#B0D9CB
#D9CAAE
#9788C7
*/
@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { HomeComponent } from './home.component';
describe('HomeComponent', () => {
let component: HomeComponent;
let fixture: ComponentFixture<HomeComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [HomeComponent]
})
.compileComponents();
fixture = TestBed.createComponent(HomeComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
+74
View File
@@ -0,0 +1,74 @@
import { DatePipe } from '@angular/common';
import { Title } from '@angular/platform-browser';
import { trigger, state, style, animate, transition } from '@angular/animations';
import { Component, DestroyRef, inject, OnInit } from '@angular/core';
import { MatCardModule } from '@angular/material/card';
import { MatDividerModule } from '@angular/material/divider';
import { MatIconModule } from '@angular/material/icon';
import { ListErrorsComponent } from '@components/shared';
import { Errors } from '@models';
//import { UtilitiesService } from '@services';
//import { UserService } from '@services';
@Component({
selector: 'app-home',
standalone: true,
imports: [
DatePipe,
MatCardModule, MatDividerModule, MatIconModule,
ListErrorsComponent
],
templateUrl: './home.component.html',
styleUrl: './home.component.scss',
animations: [
trigger('flyInOut', [
state('in', style({ transform: 'translateY(0)' })),
transition('void => *', [
style({ transform: 'translateY(-100%)' }),
animate(200)
]),
transition('* => void', [
style({ transform: 'translateY(100%)' }),
animate(200)
])
])
]
})
export class HomeComponent implements OnInit {
public title = 'Shop bientôt disponible!';
public description = 'Encore un peu de patience, notre shop sera mis en ligne d\'ici peu.';
public errors: Errors = { errors: {} };
public destroyRef = inject(DestroyRef);
public now = new Date();
constructor(
private _titleService: Title,
//private _utilitiesService: UtilitiesService
//private _userService: UserService
) { }
ngOnInit() {
this._titleService.setTitle(this.title);
/*
type CreateArrayWithLengthX<
LENGTH extends number,
ACC extends unknown[] = [],
> = ACC['length'] extends LENGTH
? ACC
: CreateArrayWithLengthX<LENGTH, [...ACC, 1]>;
type NumericRange<
START_ARR extends number[],
END extends number,
ACC extends number = never>
= START_ARR['length'] extends END
? ACC | END
: NumericRange<[...START_ARR, 1], END, ACC | START_ARR['length']>;
//type TWENTY_TO_FORTY = NumericRange<CreateArrayWithLengthX<7>, 13, 7>;
//type TWENTY_TO_FORTY = NumericRange<CreateArrayWithLengthX<13>, 19>;
type TWENTY_TO_FORTY = NumericRange<CreateArrayWithLengthX<1>, 10>;
*/
}
}
+22
View File
@@ -0,0 +1,22 @@
export * from './auth/auth.component';
export * from './aeronefs/aeronefs.component';
export * from './calculator/calculator.component';
export * from './canopies/canopies.component';
export * from './dashboard/dashboard.component';
export * from './demo/demo.component';
export * from './dropzones/dropzones.component';
export * from './herowars/herowars.component';
export * from './herowars-guildwar/herowars-guildwar.component';
export * from './herowars-guildraid/herowars-guildraid.component';
export * from './home/home.component';
export * from './jump/jump.component';
export * from './jumps/jumps.component';
export * from './logbook/logbook.component';
export * from './page/page.component';
export * from './pages/pages.component';
export * from './product/product.component';
export * from './products/products.component';
export * from './profile/profile.component';
export * from './qcm/qcm.component';
export * from './settings';
export * from './shared';
+283
View File
@@ -0,0 +1,283 @@
<div class="d-flex">
<div class="flex-fill">
<h1 class="mb-1">
{{title}}
<span class="fs-3 text-accent ms-3">{{ jump.categorie }}</span>
@if (jump.module) {
<span class="fs-3 fw-light mx-2">/</span>
<span class="fs-3 text-muted">{{ jump.module }}</span>
}
</h1>
<span class="me-2 fs-6">
Le <span class="fs-5 mx-1 text-primary">{{ jump.date | date: 'dd/MM/yyyy' }}</span>
à <span class="fs-5 mx-1 text-primary">{{ jump.lieu }} {{ jump.oaci }}</span>
</span>
</div>
<span class="flex-spacer"></span>
<div class="align-self-center me-4">
<button mat-button [routerLink]="['/jump', prevSlug]" aria-label="Saut précédent" color="accent">
<mat-icon fontIcon="navigate_before"></mat-icon> {{ jump.numero - 1}}
</button>
<div class="vr mx-2 fs-3 align-middle"></div>
<span class="mx-2 align-middle">{{ jump.numero }}</span>
<div class="vr mx-2 fs-3 align-middle"></div>
<button mat-button [routerLink]="['/jump', nextSlug]" aria-label="Saut suivant" color="accent">
<mat-icon fontIcon="navigate_next" iconPositionEnd></mat-icon> {{ jump.numero +1}}
</button>
</div>
<div class="align-self-center me-2">
<button mat-icon-button [matMenuTriggerFor]="menuJump" aria-label="Menu saut">
<mat-icon fontIcon="more_vert"></mat-icon>
</button>
<mat-menu #menuJump="matMenu">
@if (jump.x2data) {
<button mat-menu-item>
<mat-icon fontIcon="language" color="accent"></mat-icon>
<span class="text-accent fw-medium">Obtenir le KML</span>
</button>
<mat-divider></mat-divider>
}
@for (menuitem of menuItems.getMenuJump(); track menuitem) {
<button mat-menu-item>
<mat-icon [fontIcon]="menuitem.icon" [color]="menuitem.color"></mat-icon>
<span class="text-{{menuitem.color}} fw-medium">{{ menuitem.name }}</span>
</button>
}
</mat-menu>
</div>
</div>
<mat-divider class="my-3"></mat-divider>
<mat-card>
<mat-card-content>
<div class="row">
<div class="col-lg-6 col-md-12">
<dl class="dl-horizontal">
<dt class="text-light-info fw-normal">Aeronef:</dt>
<dd>{{ jump.aeronef }} {{ jump.imat }}</dd>
<dt class="text-light-info fw-normal">Voile:</dt>
<dd>{{ jump.voile }} {{ jump.taille }} ft<sup>2</sup></dd>
@if (jump.programme) {
<dt class="text-light-info fw-normal">Programme:</dt>
<dd>{{ jump.programme }}</dd>
}
</dl>
</div>
<div class="col-lg-6 col-md-12">
<dl class="dl-horizontal">
@if (!jump.x2data) {
<dt class="text-light-info fw-normal">Hauteur:</dt>
<dd>{{ jump.hauteur }} m</dd>
<dt class="text-light-info fw-normal">Déploiement:</dt>
<dd>{{ jump.deploiement }} m</dd>
}
@for (file of jump.files; track file) {
<dt class="text-light-info fw-normal">Fichier {{ file.type }}</dt>
<dd>{{ file.name }}</dd>
}
</dl>
</div>
</div>
</mat-card-content>
</mat-card>
@if (jump.x2data) {
<mat-card class="mt-3">
<mat-card-content>
<div class="row my-3">
<div class="col-sm-4 col-xs-12 text-center">
<h6>Hauteur</h6>
@if (jump.x2data.altitude.exit > 4200) {
<mat-icon class="align-middle fs-5 me-2" fontIcon="mood" color="primary"></mat-icon>
}
<span class="text-primary fw-medium fs-5">{{ jump.x2data.altitude.exit }} m</span>
</div>
<div class="col-sm-4 col-xs-12 text-center">
<h6 class="d-flex justify-content-center"><mat-icon fontIcon="timer" class="me-1 fs-5"></mat-icon> Durée de la chute</h6>
<span class="text-primary fw-medium fs-4">{{ formatDuration(jump.x2data.duration.freeFall) }}</span>
</div>
<div class="col-sm-4 col-xs-12 text-center">
<h6>Déploiement</h6>
@if (jump.x2data.altitude.deployment < 650) {
<mat-icon class="align-middle fs-5 me-2" fontIcon="warning" color="raspberry"></mat-icon>
<span class="text-raspberry fw-medium fs-5">{{ jump.x2data.altitude.deployment }} m</span>
}
@else if (jump.x2data.altitude.deployment < 750) {
<mat-icon class="align-middle fs-5 me-2" fontIcon="local_florist" color="warn"></mat-icon>
<span class="text-warn fw-medium fs-5">{{ jump.x2data.altitude.deployment }} m</span>
}
@else if (jump.x2data.altitude.deployment > 1000) {
<mat-icon class="align-middle fs-5 me-2" fontIcon="mood" color="primary"></mat-icon>
<span class="text-primary fw-medium fs-5">{{ jump.x2data.altitude.deployment }} m</span>
}
</div>
</div>
<div class="row mt-4">
<div class="{{ jump.accessoires ? 'col-sm-6 ' : '' }}col-xs-12 text-center">
<h5 class="d-flex justify-content-center mb-4"><mat-icon fontIcon="people" class="me-2"></mat-icon> Participants ({{ jump.sautants.length + 1 }})</h5>
@for (sautant of jump.sautants; track sautant) {
<span class="badge rounded-pill bg-primary mb-2 me-1 pe-3 py-1 fw-normal text-dark">
<mat-icon class="align-middle me-1" fontIcon="person"></mat-icon>
{{ sautant }}
</span>
}
<span class="badge rounded-pill bg-accent mb-2 me-1 pe-3 py-1 fw-normal text-dark">
<mat-icon class="align-middle me-1" fontIcon="person"></mat-icon>
{{ jump.author.username }}
</span>
</div>
@if (jump.accessoires) {
<div class="col-sm-6 col-xs-12 text-center">
<h5 class="d-flex justify-content-center mb-4"><mat-icon fontIcon="auto_awesome" class="me-2"></mat-icon> Accessoires</h5>
<span class="badge rounded-pill bg-navy-light mb-2 me-1 pe-3 py-1 fw-normal">
<mat-icon class="align-middle me-1" fontIcon="auto_awesome"></mat-icon>
{{ jump.accessoires }}
</span>
</div>
}
</div>
</mat-card-content>
</mat-card>
<mat-card class="mt-3">
<mat-card-content>
<div class="row my-3">
<div class="col-xs-12 text-center">
<h5 class="d-flex justify-content-center mb-3"><mat-icon fontIcon="speed" class="me-1"></mat-icon> Vitesse en chute</h5>
</div>
<div class="col-sm-6 hidden-xs text-center">
<h6>Verticale</h6>
</div>
<div class="col-sm-6 hidden-xs text-center">
<h6>Horizontale</h6>
</div>
<div class="col-sm-3 col-xs-12 text-center">
<h6>Vitesse maximale</h6>
<span class="text-primary fw-medium fs-5">{{ (jump.x2data.speed.freeFall.vertical.max / 1000 * 60 * 60) | number : '1.0-0' }} km/h</span>
</div>
<div class="col-sm-3 col-xs-12 text-center">
<h6>Vitesse moyenne</h6>
<span class="text-primary fw-medium fs-5">{{ (jump.x2data.speed.freeFall.vertical.avg / 1000 * 60 * 60) | number : '1.0-0' }} km/h</span>
</div>
<div class="col-sm-3 col-xs-12 text-center">
<h6>Vitesse maximale</h6>
<span class="text-primary fw-medium fs-5">{{ (jump.x2data.speed.freeFall.horizontal.max / 1000 * 60 * 60) | number : '1.0-0' }} km/h</span>
</div>
<div class="col-sm-3 col-xs-12 text-center">
<h6>Vitesse moyenne</h6>
<span class="text-primary fw-medium fs-5">{{ (jump.x2data.speed.freeFall.horizontal.avg / 1000 * 60 * 60) | number : '1.0-0' }} km/h</span>
</div>
</div>
</mat-card-content>
</mat-card>
<mat-card class="mt-3">
<mat-card-content>
<div class="row my-3">
<div class="col-xs-12 text-center">
<h5 class="d-flex justify-content-center mb-3"><mat-icon fontIcon="speed" class="me-1"></mat-icon> Vitesse sous voile</h5>
</div>
<div class="col-sm-6 hidden-xs text-center">
<h6>Verticale</h6>
</div>
<div class="col-sm-6 hidden-xs text-center">
<h6>Horizontale</h6>
</div>
<div class="col-sm-3 col-xs-12 text-center">
<h6>Vitesse maximale</h6>
<span class="text-primary fw-medium fs-5">{{ (jump.x2data.speed.underCanopy.vertical.max / 1000 * 60 * 60) | number : '1.0-0' }} km/h</span>
</div>
<div class="col-sm-3 col-xs-12 text-center">
<h6>Vitesse moyenne</h6>
<span class="text-primary fw-medium fs-5">{{ (jump.x2data.speed.underCanopy.vertical.avg / 1000 * 60 * 60) | number : '1.0-0' }} km/h</span>
</div>
<div class="col-sm-3 col-xs-12 text-center">
<h6>Vitesse maximale</h6>
<span class="text-primary fw-medium fs-5">{{ (jump.x2data.speed.underCanopy.horizontal.max / 1000 * 60 * 60) | number : '1.0-0' }} km/h</span>
</div>
<div class="col-sm-3 col-xs-12 text-center">
<h6>Vitesse moyenne</h6>
<span class="text-primary fw-medium fs-5">{{ (jump.x2data.speed.underCanopy.horizontal.avg / 1000 * 60 * 60) | number : '1.0-0' }} km/h</span>
</div>
</div>
</mat-card-content>
</mat-card>
<mat-card class="mt-3">
<mat-card-content>
<div class="row my-3">
<div class="col-xs-12 text-center">
<h5 class="d-flex justify-content-center mb-3"><mat-icon fontIcon="square_foot" class="me-1"></mat-icon> Finesse</h5>
</div>
<div class="col-sm-6 hidden-xs text-center">
<h6>En chute</h6>
</div>
<div class="col-sm-6 hidden-xs text-center">
<h6>Sous voile</h6>
</div>
<div class="col-sm-3 col-xs-12 text-center">
<h6>Finesse Max</h6>
<span class="text-primary fw-medium fs-5">{{ jump.x2data.glideRatio.freeFall.max | number : '1.2-2' }}</span>
</div>
<div class="col-sm-3 col-xs-12 text-center">
<h6>Finesse Moyenne</h6>
<span class="text-primary fw-medium fs-5">{{ jump.x2data.glideRatio.freeFall.avg | number : '1.2-2' }}</span>
</div>
<div class="col-sm-3 col-xs-12 text-center">
<h6>Finesse Max</h6>
<span class="text-primary fw-medium fs-5">{{ jump.x2data.glideRatio.underCanopy.max | number : '1.2-2' }}</span>
</div>
<div class="col-sm-3 col-xs-12 text-center">
<h6>Finesse Moyenne</h6>
<span class="text-primary fw-medium fs-5">{{ jump.x2data.glideRatio.underCanopy.avg | number : '1.2-2' }}</span>
</div>
</div>
</mat-card-content>
</mat-card>
<mat-card class="mt-3">
<mat-card-content>
<div class="row my-3">
<div class="col-xs-12 text-center">
<h5 class="d-flex justify-content-center mb-3"><mat-icon fontIcon="straighten" class="me-1"></mat-icon> Distance</h5>
</div>
<div class="col-sm-3 col-xs-12 text-center">
<h6>Distance au largage</h6>
<span class="text-primary fw-medium fs-5">{{ jump.x2data.distance.exitFromLandingZone| number : '1.0-0' }} m</span>
</div>
<div class="col-sm-3 col-xs-12 text-center">
<h6>Durée sous voile</h6>
<span class="text-primary fw-medium fs-5">{{ formatDuration(jump.x2data.duration.underCanopy) }}</span>
</div>
<div class="col-sm-3 col-xs-12 text-center">
<h6>Distance totale</h6>
<span class="text-primary fw-medium fs-5">{{ jump.x2data.distance.totalFlying | number : '1.0-0' }} m</span>
</div>
<div class="col-sm-3 col-xs-12 text-center">
<h6>Distance horizontale</h6>
<span class="text-primary fw-medium fs-5">{{ jump.x2data.distance.totalHorizontal | number : '1.0-0' }} m</span>
</div>
</div>
</mat-card-content>
</mat-card>
}
@else {
<mat-card class="mt-3">
<mat-card-content>
<div class="row my-3">
<div class="col-xs-12 text-center">
<h5 class="d-flex justify-content-center mb-4"><mat-icon fontIcon="people" class="me-1"></mat-icon> Participants ({{ jump.sautants.length + 1 }})</h5>
@for (sautant of jump.sautants; track sautant) {
<span class="badge rounded-pill bg-primary mb-2 me-1 pe-3 py-1 fw-normal text-dark">
<mat-icon class="align-middle me-1" fontIcon="person"></mat-icon>
{{ sautant }}
</span>
}
<span class="badge rounded-pill bg-accent mb-2 me-1 pe-3 py-1 fw-normal text-dark">
<mat-icon class="align-middle me-1" fontIcon="person"></mat-icon>
{{ jump.author.username }}
</span>
</div>
</div>
</mat-card-content>
</mat-card>
<mat-card class="mt-3">
<mat-card-content>
<div class="d-flex justify-content-center my-3 fs-5"><mat-icon fontIcon="satellite_alt" class="me-2"></mat-icon> Aucune donnée GPS</div>
</mat-card-content>
</mat-card>
}
@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { JumpComponent } from './jump.component';
describe('JumpComponent', () => {
let component: JumpComponent;
let fixture: ComponentFixture<JumpComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [JumpComponent]
})
.compileComponents();
fixture = TestBed.createComponent(JumpComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
+71
View File
@@ -0,0 +1,71 @@
import { Component, DestroyRef, inject, OnInit, OnDestroy } from '@angular/core';
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { DatePipe, DecimalPipe } from '@angular/common';
import { ActivatedRoute, RouterLink } from '@angular/router';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatDividerModule } from '@angular/material/divider';
import { MatIconModule } from '@angular/material/icon';
import { MatMenuModule } from '@angular/material/menu';
import { Observable, Subscription } from 'rxjs';
import { ListErrorsComponent, MenuItems } from '@components/shared';
import { Errors, Jump, JumpPageData } from '@models';
@Component({
selector: 'app-jump',
standalone: true,
imports: [
DatePipe, DecimalPipe, RouterLink,
MatButtonModule, MatCardModule,
MatDividerModule, MatIconModule, MatMenuModule,
ListErrorsComponent
],
templateUrl: './jump.component.html',
styleUrl: './jump.component.scss'
})
export class JumpComponent implements OnInit, OnDestroy {
private _data: Subscription = new Subscription();
public title: string = '';
public errors: Errors = { errors: {} };
public destroyRef = inject(DestroyRef);
public jump: Jump = {} as Jump;
public prevSlug: string = '';
public nextSlug: string = '';
constructor(
private route: ActivatedRoute,
public menuItems: MenuItems
) { }
ngOnInit() {
const data$: Observable<{ pageData: JumpPageData }> = this.route.data as Observable<{ pageData: JumpPageData }>;
this._data = data$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((data: { pageData: JumpPageData }) => {
this.jump = data.pageData.jump;
this.prevSlug = data.pageData.prevJump?.slug;
this.nextSlug = data.pageData.nextJump?.slug;
this.title = `Saut n° ${this.jump.numero} `;
});
/*
const data$: Observable<Jump> = this.route.data as Observable<Jump>;
this._data = data$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((data: Jump) => {
this.jump = data;
console.log('jump : ', this.jump);
});
*/
}
ngOnDestroy() {
this._data.unsubscribe();
}
formatDuration(duration: number): string {
const start = new Date(0);
start.setSeconds(duration);
return start.toISOString().substring(14, 19);
}
private _resetErrors(): void {
this.errors = { errors: {} };
}
}
@@ -0,0 +1,130 @@
<div @flyInOut>
<div class="d-flex">
<div class="flex-fill">
<h1 class="mb-1">{{title}}</h1>
<span [hidden]="!lastJump.numero" class="me-2 fs-6">
Dernier saut enregistré : <span class="fs-5 mx-1 text-primary">{{ lastJump.numero }}<sup>ème</sup></span> à <span class="fs-5 mx-1 text-primary">{{ lastJump.lieu }}</span> le <span class="fs-5 mx-1 text-primary">{{ lastJump.date | date: 'dd/MM/yyyy' }}</span>
</span>
</div>
<span class="flex-spacer"></span>
<div class="align-self-center me-2">
<button mat-icon-button [matMenuTriggerFor]="menuJumps" aria-label="Menu sauts">
<mat-icon fontIcon="more_vert"></mat-icon>
</button>
<mat-menu #menuJumps="matMenu">
@for (menuitem of menuItems.getMenuPanel(); track menuitem) {
<button mat-menu-item>
<mat-icon [fontIcon]="menuitem.icon"></mat-icon>
<span>{{ menuitem.name }}</span>
</button>
}
</mat-menu>
</div>
</div>
<mat-divider class="my-3"></mat-divider>
<mat-card>
<mat-card-header>
<mat-card-title>{{ subtitle }}</mat-card-title>
</mat-card-header>
<mat-card-content class="mt-2">
@if(displayCharts) {
<div class="row">
<div class="col-xxl-6 col-xs-12">
<app-linearea-chart [headers]="seriesHeader" [names]="seriesName" [values]="seriesRow" [colors]="seriesColor"></app-linearea-chart>
</div>
<div class="col-xxl-6 col-xs-12">
<app-bars-chart [headers]="seriesHeader" [names]="seriesName" [values]="seriesRow" [colors]="seriesColor" [legend]="true"></app-bars-chart>
</div>
</div>
}
<div class="row">
<div class="col-xs-12">
<mat-divider class="mt-3 mb-0"></mat-divider>
<table class="table table-striped table-dark table-hover">
<thead>
<tr>
<th></th>
<th *ngFor='let name of seriesHeader; let i=index' class="text-end"> {{ name }} </th>
<th class="text-end">Total</th>
</tr>
</thead>
<tbody>
<tr *ngFor='let values of seriesRow; let i=index'>
<th class="{{seriesColorClass[i]}} text-end"> ● {{ seriesName[i] }} </th>
<td *ngFor='let value of values; let i=index' class="font-monospace text-end">
<span *ngIf="value; else elseBlock" class="pr-1">{{ value }}</span>
<ng-template #elseBlock><span class="text-empty pr-1">{{ value }}</span></ng-template>
</td>
<th class="{{seriesColorClass[i]}} font-monospace text-end">{{ seriesRowTotal[i] }}</th>
</tr>
</tbody>
<tfoot>
<tr>
<th class="text-end">Total</th>
<th *ngFor='let value of seriesColTotal; let i=index' class="font-monospace text-end">
<span *ngIf="value; else elseBlock" class="pr-1">{{ value }}</span>
<ng-template #elseBlock><span class="text-empty pr-1">{{ value }}</span></ng-template>
</th>
<th class="font-monospace text-end">{{ grandTotal }}</th>
</tr>
<tr>
<th class="text-end">Moyenne</th>
<th *ngFor='let value of seriesRowAvg; let i=index' class="font-monospace text-end">
<span *ngIf="value; else elseBlock" class="pr-1">{{ value | number : '1.0-1' }}</span>
<ng-template #elseBlock><span class="text-empty pr-1">{{ value | number : '1.0-1' }}</span></ng-template>
</th>
<th class="font-monospace text-end">{{ grandAvg | number : '1.0-1' }}</th>
</tr>
<tr>
<th class="text-end">Moyenne <small>3 dernières années</small></th>
<th *ngFor='let value of seriesRowAvgLastYears; let i=index' class="font-monospace text-end">
<span *ngIf="value; else elseBlock" class="pr-1">{{ value | number : '1.0-1' }}</span>
<ng-template #elseBlock><span class="text-empty pr-1">{{ value | number : '1.0-1' }}</span></ng-template>
</th>
<th class="font-monospace text-end">{{ grandAvgLastYears | number : '1.0-1' }}</th>
</tr>
</tfoot>
</table>
</div>
</div>
</mat-card-content>
</mat-card>
<mat-card class="bannercard">
<mat-card-header>
<mat-card-title>Types de sauts</mat-card-title>
</mat-card-header>
<mat-card-content class="mt-2">
@if(displayCharts) {
<div class="row">
<div class="col-xs-12">
<app-line-chart [headers]="seriesTypesHeader" [names]="seriesTypesName" [values]="seriesTypesRowCumulated" [colors]="seriesTypesColor" [legend]="true" [hideZero]="true"></app-line-chart>
</div>
</div>
<mat-divider class="mt-2 mb-3"></mat-divider>
<div class="row">
@for (item of getCategories(); track $index) {
<div class="col-lg-2 col-sm-4 col-xs-6">
<app-circle-chart [headers]="seriesHeader" [names]="seriesName" [values]="[item.count, (lastJump.numero-item.count)]" [color]="$index" [label]="item.categorie"></app-circle-chart>
</div>
}
@for (item of getModules(); track $index) {
<div class="col-lg-2 col-sm-4 col-xs-6">
<app-circle-chart [headers]="seriesHeader" [names]="seriesName" [values]="[item.count, (lastJump.numero-item.count)]" [color]="getModuleColor($index)" [label]="item.module"></app-circle-chart>
</div>
}
</div>
<mat-divider class="my-3"></mat-divider>
<!--
<div class="row">
<div class="col-xxl-6 col-xs-12">
<app-line-chart [headers]="seriesTypesHeader" [names]="seriesTypesName" [values]="seriesTypesRow" [colors]="seriesTypesColor" [legend]="true"></app-line-chart>
</div>
<div class="col-xxl-6 col-xs-12">
<app-line-chart [headers]="seriesTypesHeader" [names]="seriesTypesName" [values]="seriesTypesRowCumulated" [colors]="seriesTypesColor" [legend]="true"></app-line-chart>
</div>
</div>
-->
}
</mat-card-content>
</mat-card>
</div>
@@ -0,0 +1,12 @@
element.style {
margin: 1.5rem -1.5rem -1.5rem -1.5rem;
padding: 0.9rem 1.1rem;
border-radius: 0;
}
.bannercard {
background-color: #202020;
color: #ffffff;
margin: 1.5rem -1.5rem -1.5rem -1.5rem;
padding: 0.9rem 1.1rem;
border-radius: 0;
}
@@ -0,0 +1,21 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { JumpsComponent } from './jumps.component';
describe('JumpsComponent', () => {
let component: JumpsComponent;
let fixture: ComponentFixture<JumpsComponent>;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [JumpsComponent]
});
fixture = TestBed.createComponent(JumpsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
+249
View File
@@ -0,0 +1,249 @@
import { trigger, state, style, animate, transition } from '@angular/animations';
import { Component, DestroyRef, inject, OnInit, OnDestroy } from '@angular/core';
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { ActivatedRoute } from '@angular/router';
import { CommonModule } from '@angular/common';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatDividerModule } from '@angular/material/divider';
import { MatExpansionModule } from '@angular/material/expansion';
import { MatIconModule } from '@angular/material/icon';
import { MatMenuModule } from '@angular/material/menu';
import { Observable, Subscription } from 'rxjs';
import { MenuItems } from '@components/shared';
import { BarsChartComponent, CircleChartComponent, LineChartComponent, LineAreaChartComponent } from '@components/shared/helpers-chart';
import { UtilitiesService } from '@services';
import { Jump, JumpByCategorie, JumpByDate, JumpByDateByModule, JumpByModule, JumpsPageData, JumpYears } from '@models';
@Component({
selector: 'app-jumps',
standalone: true,
imports: [
CommonModule,
MatButtonModule, MatCardModule, MatDividerModule,
MatExpansionModule, MatIconModule, MatMenuModule,
BarsChartComponent, CircleChartComponent, LineChartComponent, LineAreaChartComponent
],
templateUrl: './jumps.component.html',
styleUrl: './jumps.component.scss',
animations: [
trigger('flyInOut', [
state('in', style({ transform: 'translateX(0)' })),
transition('void => *', [
style({ transform: 'translateX(-100%)' }),
animate(200)
]),
transition('* => void', [
style({ transform: 'translateX(100%)' }),
animate(200)
])
])
]
})
export class JumpsComponent implements OnInit, OnDestroy {
private _data: Subscription = new Subscription();
private _jumpsByCategorie: Array<JumpByCategorie> = [];
private _jumpsByModule: Array<JumpByModule> = [];
private _jumpsByDate: Array<JumpByDate> = [];
public lastJump: Jump = {} as Jump;
public title: string = 'Sauts';
public subtitle: string = 'Nombre total de sauts par mois';
public displayCharts = false;
public destroyRef = inject(DestroyRef);
public min = 0;
public max = 0;
public grandAvg = 0;
public grandAvgLastYears = 0;
public grandTotal = 0;
public seriesHeader: string[] = [];
public seriesName: string[] = [];
public seriesRow: Array<Array<number>> = [];
public seriesColor: {
backgroundColor: string[],
borderColor: string[]
} = {
backgroundColor: this._utilitiesService.getSeriesColors(0.8, 'all'),
borderColor: this._utilitiesService.getSeriesColors(1, 'all')
};
public seriesTypesHeader: string[] = [];
public seriesTypesName: string[] = [];
public seriesTypesRow: Array<Array<number>> = [];
public seriesTypesRowCumulated: Array<Array<number>> = [];
public seriesTypesColor: {
backgroundColor: string[],
borderColor: string[]
} = {
backgroundColor: this._utilitiesService.getSeriesColors(0.8, 'pastels'),
borderColor: this._utilitiesService.getSeriesColors(1, 'pastels')
};
public seriesColorClass: string[] = this._utilitiesService.getChartColors();
public seriesRowTotal: number[] = [];
public seriesRowAvg: number[] = [];
public seriesRowAvgLastYears: number[] = [];
public seriesColTotal: number[] = [];
public seriesColTotalClosed: number[] = [];
public seriesColTotalLastYears: number[] = [];
constructor(
private route: ActivatedRoute,
private _utilitiesService: UtilitiesService,
public menuItems: MenuItems
) { }
ngOnInit() {
const data$: Observable<{ pageData: JumpsPageData }> = this.route.data as Observable<{ pageData: JumpsPageData }>;
this._data = data$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((data: { pageData: JumpsPageData }) => {
const pageData: JumpsPageData = data.pageData;
this.lastJump = pageData.lastjump;
const currentYear: number = new Date().getFullYear();
const types: Array<string> = ['FF', 'Solo', 'VR', 'Wingsuit'];
const verticals: Array<string> = ['Chutas', 'Head Up', 'Head Down'];
const others: Array<string> = ['Track', 'Trace'];
const modules: Array<string> = [...verticals, ...others];
const minoration = 2; // -2 pour ne pas compter la première année
this.min = pageData.jumpsByYears[0].year;
this.max = pageData.jumpsByYears[(pageData.jumpsByYears.length-1)].year;
this.seriesHeader = this._utilitiesService.getMonthsList();
const values: Array<number> = this.seriesHeader.map(() => 0);
this.seriesColTotal = [...values];
this.seriesColTotalClosed = [...values];
this.seriesColTotalLastYears = [...values];
this.seriesName = pageData.jumpsByYears.map((row: JumpYears) => {
this.seriesRow.push([...values]);
return row.year.toString();
});
this._jumpsByDate = pageData.jumpsByDate.map((row: JumpByDate) => {
this.seriesRow[(row.year-this.min)][(row.month-1)] = row.count;
this.seriesColTotal[(row.month-1)] += row.count;
if (row.year < currentYear) {
this.seriesColTotalClosed[(row.month-1)] += row.count;
}
if (row.year >= (currentYear-3) && row.year < currentYear) {
this.seriesColTotalLastYears[(row.month-1)] += row.count;
}
return row;
});
this.seriesRow.forEach((row: number[]) => {
const total = row.reduce((partialSum, a) => partialSum + a, 0);
this.seriesRowTotal.push(total);
});
this.seriesColTotalClosed.forEach((row: number) => {
const value: number = (row/(this.seriesName.length-minoration));
this.seriesRowAvg.push(parseInt(value.toFixed(0)));
});
this.seriesColTotalLastYears.forEach((row: number) => {
const value: number = (row/3); // Les 3 dernières années
this.seriesRowAvgLastYears.push(parseInt(value.toFixed(0)));
});
this.grandAvg = this.seriesRowAvg.reduce((partialSum, accumulated) => partialSum + accumulated, 0);
this.grandAvgLastYears = this.seriesRowAvgLastYears.reduce((partialSum, accumulated) => partialSum + accumulated, 0);
this.grandTotal = this.seriesColTotal.reduce((partialSum, accumulated) => partialSum + accumulated, 0);
pageData.jumpsByCategory.forEach((row: JumpByCategorie) => {
if (types.indexOf(row.categorie) !== -1) {
this._jumpsByCategorie.push(row);
}
});
//const focus: Array<string> = ['FF', 'Solo']; // ['FF']
let verticalCount:number = 0;
let otherCount:number = 0;
pageData.jumpsByModule.forEach((row: JumpByModule) => {
//if (focus.indexOf(row.categorie) !== -1 && modules.indexOf(row.module) !== -1) {
if (modules.indexOf(row.module) !== -1) {
if (verticals.indexOf(row.module) !== -1) {
verticalCount += row.count;
} else {
otherCount += row.count;
}
}
});
if (verticalCount > 0) {
const row:JumpByModule = {
categorie: 'FF',
module: 'Vertical',
count: verticalCount
}
this._jumpsByModule.push(row);
}
if (otherCount > 0) {
const row:JumpByModule = {
categorie: 'FF',
module: 'Track/Trace',
count: otherCount
}
this._jumpsByModule.push(row);
}
for (let year = 0; year < pageData.jumpsByYears.length; year++) {
for (let index = 0; index < 12; index++) {
const month: string = ((index+1) < 10) ? `0${(index+1)}` : `${(index+1)}`;
this.seriesTypesHeader.push(`${month}-${pageData.jumpsByYears[year].year}`);
}
}
const emptyValues: Array<number> = this.seriesTypesHeader.map(() => 0);
this.seriesTypesName = types;
this.seriesTypesName.forEach(() => {
this.seriesTypesRow.push([...emptyValues]);
});
pageData.jumpsByDateByModule.forEach((row: JumpByDateByModule) => {
const indexOf: number = this.seriesTypesName.indexOf(row.categorie);
if (indexOf !== -1) {
this.seriesTypesRow[indexOf][(((row.year-this.min)*12)+row.month-1)] += row.count;
}
});
this.seriesTypesRowCumulated = this.seriesTypesRow.map((row: Array<number>) => {
let accumulated: number = 0;
return row.map((value: number) => {
accumulated += value;
return accumulated;
});
});
this.seriesTypesHeader = this.seriesTypesHeader.slice((pageData.jumpsByDateByModule[0].month+1), -(pageData.jumpsByDateByModule[(pageData.jumpsByDateByModule.length-1)].month));
this.seriesTypesRow = this.seriesTypesRow.map((row: Array<number>) => {
return row.slice((pageData.jumpsByDateByModule[0].month+1), -(pageData.jumpsByDateByModule[(pageData.jumpsByDateByModule.length-1)].month));
});
this.seriesTypesRowCumulated = this.seriesTypesRowCumulated.map((row: Array<number>) => {
return row.slice((pageData.jumpsByDateByModule[0].month+1), -(pageData.jumpsByDateByModule[(pageData.jumpsByDateByModule.length-1)].month));
});
/*
console.log('this.min', this.min);
console.log('this.max', this.max);
console.log('this.lastJump', this.lastJump);
console.log('this.seriesHeader', this.seriesHeader);
console.log('this.seriesName', this.seriesName);
console.log('this.seriesRow', this.seriesRow);
console.log('this.seriesRowTotal', this.seriesRowTotal);
console.log('this.seriesRowAvg', this.seriesRowAvg);
console.log('this.seriesRowAvgLastYears', this.seriesRowAvgLastYears);
console.log('this._jumpsByCategorie', this._jumpsByCategorie);
console.log('this._jumpsByModule', this._jumpsByModule);
console.log('this.seriesTypesHeader', this.seriesTypesHeader);
console.log('this.seriesTypesName', this.seriesTypesName);
console.log('this.seriesTypesRow', this.seriesTypesRow);
console.log('this.seriesTypesRowCumulated', this.seriesTypesRowCumulated);
*/
this.displayCharts = true;
});
}
ngOnDestroy() {
this._data.unsubscribe();
}
public getCategories(): Array<JumpByCategorie> {
return this._jumpsByCategorie;
}
public getModules(): Array<JumpByModule> {
return this._jumpsByModule;
}
public getModuleColor(index: number): number {
return (this._jumpsByCategorie.length + index);
}
}
@@ -0,0 +1,4 @@
export * from './jump-add.dialog';
export * from './jump-delete.dialog';
export * from './jump-edit.dialog';
export * from './jump-view.dialog';
@@ -0,0 +1,301 @@
<h2 mat-dialog-title class="bg-warn border-bottom border-light-subtle d-flex mb-3 pt-3 pb-2">
Ajouter un saut<span class="flex-spacer"></span>
<mat-icon fontIcon="close" class="pointer" (click)="closeDialog()"></mat-icon>
</h2>
<mat-dialog-content>
<div class="container">
<!--
<div class="row">
<div class="col-xs-12 border-bottom border-dark pb-2 mb-2">
<mat-form-field appearance="outline" class="mt-2 me-2" color="warn">
<mat-label>Numéro ou plage de saut à ajouter</mat-label>
<input matInput [(ngModel)]="params.numeros" required />
<mat-hint>645 / +5 / 650-660</mat-hint>
</mat-form-field>
<mat-form-field appearance="outline" class="mt-2 me-3" color="warn">
<mat-label>Choisissez une date</mat-label>
<input matInput [matDatepicker]="datepicker" [(ngModel)]="params.date" (dateChange)="onDateChange()" required />
<mat-hint>MM/DD/YYYY</mat-hint>
<mat-datepicker-toggle matIconSuffix [for]="datepicker"></mat-datepicker-toggle>
<mat-datepicker #datepicker>
<mat-datepicker-actions>
<button mat-button matDatepickerCancel>Annuler</button>
<button mat-raised-button color="warn" matDatepickerApply>Appliquer</button>
</mat-datepicker-actions>
</mat-datepicker>
</mat-form-field>
<mat-checkbox (change)="setLastJump($event.checked)" [checked]="useLast.all">
<mat-icon fontIcon="history" class="align-middle me-2"></mat-icon> <span class="d-inline-block align-middle">Utiliser les valeurs du dernier saut</span>
</mat-checkbox>
</div>
</div>
-->
<form class="row" [formGroup]="jumpForm" (ngSubmit)="submitForm()">
<div class="col-xs-12 border-bottom border-dark pb-2 mb-2">
<mat-form-field appearance="outline" class="mt-2 me-2" color="warn">
<mat-label>Numéro ou plage de saut à ajouter</mat-label>
<input matInput formControlName="numero" required />
<mat-hint>645 / +5 / 650-660</mat-hint>
</mat-form-field>
<mat-form-field appearance="outline" class="mt-2 me-3" color="warn">
<mat-label>Choisissez une date</mat-label>
<input matInput [matDatepicker]="datepicker" formControlName="date" (dateChange)="onDateChange()" required cdkFocusInitial />
<mat-hint>MM/DD/YYYY</mat-hint>
<mat-datepicker-toggle matIconSuffix [for]="datepicker"></mat-datepicker-toggle>
<mat-datepicker #datepicker>
<mat-datepicker-actions>
<button mat-button matDatepickerCancel>Annuler</button>
<button mat-raised-button color="warn" matDatepickerApply>Appliquer</button>
</mat-datepicker-actions>
</mat-datepicker>
</mat-form-field>
<mat-checkbox (change)="setLastJump($event.checked)" [checked]="useLast.all">
<mat-icon fontIcon="history" class="align-middle me-2"></mat-icon> <span class="d-inline-block align-middle">Utiliser les valeurs du dernier saut</span>
</mat-checkbox>
</div>
<div class="col-lg-6 col-md-12">
<h4 class="border-bottom border-secondary mb-3 lh-lg">
Dropzone
<mat-checkbox class="float-end" (change)="setLastDropzone($event.checked)" [checked]="useLast.dropzone">
<mat-icon fontIcon="history" class="align-middle"></mat-icon>
</mat-checkbox>
</h4>
<mat-form-field appearance="fill" class="me-2" color="warn">
<mat-label>Lieu</mat-label>
<input matInput formControlName="lieu" [matAutocomplete]="autoDropzone">
<mat-autocomplete #autoDropzone="matAutocomplete" [displayWith]="displayLieuFn">
@for (option of filteredOptions.dropzones | async; track option) {
<mat-option [value]="option" (onSelectionChange)="onSelLieu(option)">{{option.lieu}} - {{option.oaci}}</mat-option>
}
</mat-autocomplete>
@if (this.jumpForm.controls['lieu'].status === 'INVALID') {
<mat-error>{{getErrorMessage('lieu')}}</mat-error>
}
</mat-form-field>
<mat-form-field appearance="fill" class="me-2" color="warn">
<mat-label>Oaci</mat-label>
<input matInput formControlName="oaci" [matAutocomplete]="autoOaci">
<mat-autocomplete #autoOaci="matAutocomplete" [displayWith]="displayOaciFn">
@for (option of filteredOptions.oaci | async; track option) {
<mat-option [value]="option" (onSelectionChange)="onSelOaci(option)">{{option.lieu}} - {{option.oaci}}</mat-option>
}
</mat-autocomplete>
@if (this.jumpForm.controls['oaci'].status === 'INVALID') {
<mat-error>{{getErrorMessage('oaci')}}</mat-error>
}
</mat-form-field>
<h4 class="border-bottom border-secondary mb-3 lh-lg">
Aéronef
<mat-checkbox class="float-end" (change)="setLastAeronef($event.checked)" [checked]="useLast.aeronef">
<mat-icon fontIcon="history" class="align-middle"></mat-icon>
</mat-checkbox>
</h4>
<mat-form-field appearance="fill" class="me-2" color="warn">
<mat-label>Aéronef</mat-label>
<input matInput formControlName="aeronef" [matAutocomplete]="autoAeronef">
<mat-autocomplete #autoAeronef="matAutocomplete" [displayWith]="displayAeronefFn">
@for (option of filteredOptions.aeronefs | async; track option) {
<mat-option [value]="option" (onSelectionChange)="onSelAeronef(option)">{{option.aeronef}} - {{option.imat}}</mat-option>
}
</mat-autocomplete>
@if (this.jumpForm.controls['aeronef'].status === 'INVALID') {
<mat-error>{{getErrorMessage('aeronef')}}</mat-error>
}
</mat-form-field>
<mat-form-field appearance="fill" class="me-2" color="warn">
<mat-label>Imatriculation</mat-label>
<input matInput formControlName="imat" [matAutocomplete]="autoImat">
<mat-autocomplete #autoImat="matAutocomplete" [displayWith]="displayImatFn">
@for (option of filteredOptions.imats | async; track option) {
<mat-option [value]="option" (onSelectionChange)="onSelImat(option)">{{option.aeronef}} - {{option.imat}}</mat-option>
}
</mat-autocomplete>
@if (this.jumpForm.controls['imat'].status === 'INVALID') {
<mat-error>{{getErrorMessage('imat')}}</mat-error>
}
</mat-form-field>
<h4 class="border-bottom border-secondary mb-3 lh-lg">
Voile
<mat-checkbox class="float-end" (change)="setLastVoile($event.checked)" [checked]="useLast.voile">
<mat-icon fontIcon="history" class="align-middle"></mat-icon>
</mat-checkbox>
</h4>
<mat-form-field appearance="fill" class="me-2" color="warn">
<mat-label>Modèle</mat-label>
<input matInput formControlName="voile" [matAutocomplete]="autoCanopy">
<mat-autocomplete #autoCanopy="matAutocomplete" [displayWith]="displayVoileFn">
@for (option of filteredOptions.canopies | async; track option) {
<mat-option [value]="option" (onSelectionChange)="onSelVoile(option)">{{option.voile}} - {{option.taille}}</mat-option>
}
</mat-autocomplete>
@if (this.jumpForm.controls['voile'].status === 'INVALID') {
<mat-error>{{getErrorMessage('voile')}}</mat-error>
}
</mat-form-field>
<mat-form-field appearance="fill" class="me-2" color="warn">
<mat-label>Taille en ft<sup>2</sup></mat-label>
<input matInput formControlName="taille" [matAutocomplete]="autoTaille">
<mat-autocomplete #autoTaille="matAutocomplete" [displayWith]="displayTailleFn">
@for (option of filteredOptions.tailles | async; track option) {
<mat-option [value]="option" (onSelectionChange)="onSelTaille(option)">{{option.voile}} - {{option.taille}}</mat-option>
}
</mat-autocomplete>
<span matTextSuffix>ft<sup>2</sup></span>
@if (this.jumpForm.controls['taille'].status === 'INVALID') {
<mat-error>{{getErrorMessage('taille')}}</mat-error>
}
</mat-form-field>
</div>
<div class="col-lg-6 col-md-12">
<h4 class="border-bottom border-secondary mb-3 lh-lg">
Altitude
<mat-checkbox class="float-end" (change)="setLastAltitude($event.checked)" [checked]="useLast.altitude">
<mat-icon fontIcon="history" class="align-middle"></mat-icon>
</mat-checkbox>
</h4>
<mat-form-field appearance="fill" class="me-2" color="warn">
<mat-label>Hauteur de largage</mat-label>
<input matInput formControlName="hauteur" type="number" min="0" step="1">
@if (this.jumpForm.controls['hauteur'].status === 'INVALID') {
<mat-error>{{getErrorMessage('hauteur')}}</mat-error>
}
</mat-form-field>
<mat-form-field appearance="fill" class="me-2" color="warn">
<mat-label>Hauteur de déploiement</mat-label>
<input matInput formControlName="deploiement" type="number" min="0" step="1">
@if (this.jumpForm.controls['deploiement'].status === 'INVALID') {
<mat-error>{{getErrorMessage('deploiement')}}</mat-error>
}
</mat-form-field>
<h4 class="border-bottom border-secondary mb-3 lh-lg">
Discipline
<mat-checkbox class="float-end" (change)="setLastDiscipline($event.checked)" [checked]="useLast.discipline">
<mat-icon fontIcon="history" class="align-middle"></mat-icon>
</mat-checkbox>
</h4>
<mat-form-field appearance="fill" class="me-2" color="warn">
<mat-label>Categorie</mat-label>
<input matInput formControlName="categorie" [matAutocomplete]="autoCategorie">
<mat-autocomplete #autoCategorie="matAutocomplete" [displayWith]="displayCategorieFn">
@for (option of filteredOptions.categories | async; track option) {
<mat-option [value]="option" (onSelectionChange)="onSelCategorie(option)">{{option.categorie}}{{option.categorie && option.module ? ' - ' : ''}}{{option.module}}</mat-option>
}
</mat-autocomplete>
@if (this.jumpForm.controls['categorie'].status === 'INVALID') {
<mat-error>{{getErrorMessage('categorie')}}</mat-error>
}
</mat-form-field>
<mat-form-field appearance="fill" class="me-2" color="warn">
<mat-label>Module</mat-label>
<input matInput formControlName="module" [matAutocomplete]="autoModule">
<mat-autocomplete #autoModule="matAutocomplete" [displayWith]="displayModuleFn">
@for (option of filteredOptions.modules | async; track option) {
<mat-option [value]="option" (onSelectionChange)="onSelModule(option)">{{option.categorie}}{{option.categorie && option.module ? ' - ' : ''}}{{option.module}}</mat-option>
}
</mat-autocomplete>
@if (this.jumpForm.controls['module'].status === 'INVALID') {
<mat-error>{{getErrorMessage('module')}}</mat-error>
}
</mat-form-field>
<h4 class="border-bottom border-secondary mb-3 lh-lg">
Divers
<mat-checkbox class="float-end" (change)="setLastDivers($event.checked)" [checked]="useLast.divers">
<mat-icon fontIcon="history" class="align-middle"></mat-icon>
</mat-checkbox>
</h4>
<mat-form-field appearance="fill" class="me-2" color="warn">
<mat-label>Accessoires</mat-label>
<input matInput formControlName="accessoires">
@if (this.jumpForm.controls['accessoires'].status === 'INVALID') {
<mat-error>{{getErrorMessage('accessoires')}}</mat-error>
}
</mat-form-field>
<mat-form-field appearance="fill" class="me-2" color="warn">
<mat-label>Zone</mat-label>
<input matInput formControlName="zone">
@if (this.jumpForm.controls['zone'].status === 'INVALID') {
<mat-error>{{getErrorMessage('zone')}}</mat-error>
}
</mat-form-field>
</div>
<div class="col-lg-6 col-md-12">
<h4 class="border-bottom border-secondary mb-3 lh-lg">
Groupe
<mat-checkbox class="float-end" (change)="setLastGroupe($event.checked)" [checked]="useLast.groupe">
<mat-icon fontIcon="history" class="align-middle"></mat-icon>
</mat-checkbox>
</h4>
<mat-form-field appearance="fill" class="me-2" color="warn">
<mat-label>Sautants</mat-label>
<input matInput placeholder="Ajouter un sautant" [formControl]="tagField" (keyup.enter)="addSautant()">
</mat-form-field>
<mat-form-field appearance="fill" class="me-2" color="warn">
<mat-label>Nombre de participants</mat-label>
<input matInput formControlName="participants" type="number" min="0" step="1">
@if (this.jumpForm.controls['participants'].status === 'INVALID') {
<mat-error>{{getErrorMessage('participants')}}</mat-error>
}
</mat-form-field>
<div class="mb-2">
@for (sautant of jump.sautants; track sautant) {
<span class="badge rounded-pill bg-primary mb-2 me-1 pe-3 py-1 fw-normal text-dark">
<mat-icon class="align-middle pointer me-1" fontIcon="close" (click)="removeSautant(sautant)"></mat-icon>
{{ sautant }}
</span>
}
<span class="badge rounded-pill bg-accent mb-2 me-1 pe-3 py-1 fw-normal text-dark">
<mat-icon class="align-middle pointer me-1" fontIcon="person"></mat-icon>
{{ lastJump.author.username }}
</span>
</div>
</div>
<div class="col-lg-6 col-md-12">
<h4 class="border-bottom border-secondary mb-3 lh-lg">
Programme
<mat-checkbox class="float-end" (change)="setLastProgramme($event.checked)" [checked]="useLast.programme">
<mat-icon fontIcon="history" class="align-middle"></mat-icon>
</mat-checkbox>
</h4>
<mat-form-field appearance="fill" class="me-2 w-100" color="warn">
<mat-label>Programme</mat-label>
<textarea matInput formControlName="programme"></textarea>
@if (this.jumpForm.controls['programme'].status === 'INVALID') {
<mat-error>{{getErrorMessage('programme')}}</mat-error>
}
</mat-form-field>
</div>
<div class="col-lg-6 col-md-12">
<h4 class="border-bottom border-secondary mb-3 lh-lg">Vidéo</h4>
<mat-form-field appearance="fill" class="me-2" color="warn">
<mat-label>Dossier</mat-label>
<input matInput formControlName="dossier">
@if (this.jumpForm.controls['dossier'].status === 'INVALID') {
<mat-error>{{getErrorMessage('dossier')}}</mat-error>
}
</mat-form-field>
<mat-form-field appearance="fill" class="me-2" color="warn">
<mat-label>Video</mat-label>
<input matInput formControlName="video">
@if (this.jumpForm.controls['video'].status === 'INVALID') {
<mat-error>{{getErrorMessage('video')}}</mat-error>
}
</mat-form-field>
<span class="d-inline-block align-middle ms-1">{{ filepath }}{{ filename }}</span>
</div>
<div class="col-lg-6 col-md-12">
<h4 class="border-bottom border-secondary mb-3 lh-lg">Données GPS</h4>
<div class="mb-2">
<mat-checkbox (change)="setLastJump($event.checked)" [checked]="useLast.all">
<mat-icon fontIcon="language" class="align-middle me-2"></mat-icon> <span class="d-inline-block align-middle">Rechercher le fichier kml</span>
</mat-checkbox>
</div>
</div>
</form>
</div>
</mat-dialog-content>
<mat-dialog-actions>
<span class="flex-spacer"></span>
<button mat-button (click)="closeDialog()" class="me-1">Annuler</button>
<!--<button mat-button color="warn" [disabled]="!jumpForm.valid" [mat-dialog-close]="{jump: jump, params: params}" cdkFocusInitial>Enregistrer</button>-->
<button mat-button color="warn" [disabled]="!jumpForm.valid" (click)="submitForm()">Enregistrer</button>
</mat-dialog-actions>
@@ -0,0 +1,873 @@
import { Component, Inject, OnDestroy } from '@angular/core';
import { AsyncPipe, DatePipe } from '@angular/common';
import { FormBuilder, FormControl, FormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms';
import { MatAutocompleteModule } from '@angular/material/autocomplete';
import { MatButtonModule } from '@angular/material/button';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatDatepickerModule } from '@angular/material/datepicker';
import { MatDialogRef, MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatOptionModule } from '@angular/material/core';
import { MatSelectModule } from '@angular/material/select';
import { Observable, Subscription } from 'rxjs';
import { map, startWith, take } from 'rxjs/operators';
import { AeronefByImat, CanopyModelBySize, DropZoneByOaci, Errors, Jump, JumpAddParams, JumpByModule, JumpFile, JumpFileType } from '@models';
import { AeronefsService, CanopiesService, DropZonesService, JumpsService } from '@services';
@Component({
selector: 'app-jump-add-dialog',
templateUrl: 'jump-add.dialog.html',
standalone: true,
imports: [
AsyncPipe,
DatePipe,
FormsModule,
ReactiveFormsModule,
MatAutocompleteModule,
MatButtonModule,
MatCheckboxModule,
MatDatepickerModule,
MatDialogModule,
MatFormFieldModule,
MatIconModule,
MatInputModule,
MatSelectModule,
MatOptionModule
]
})
export class JumpAddDialogComponent implements OnDestroy {
private _formChanges: Array<Subscription> = [];
private _subscriptions: Array<Subscription> = [];
private _aeronefByImat: Subscription = new Subscription();
private _canopyModelBySize: Subscription = new Subscription();
private _dropZoneByOaci: Subscription = new Subscription();
private _jumpByModule: Subscription = new Subscription();
public errors!: Errors;
public tagField = new FormControl<string>('', { nonNullable: true});
public jumpForm: FormGroup;
public jump: Jump;
public lastJump: Jump;
public nextJump: number;
public params: JumpAddParams;
public filename: string;
public filepath: string;
public useLast: {
all: boolean;
aeronef: boolean;
altitude: boolean;
discipline: boolean;
divers: boolean;
dropzone: boolean;
groupe: boolean;
programme: boolean;
voile: boolean;
} = {
all: false,
aeronef: false,
altitude: false,
discipline: false,
divers: false,
dropzone: false,
groupe: false,
programme: false,
voile: false
};
public inputOptions: {
aeronefs: Array<AeronefByImat>;
canopies: Array<CanopyModelBySize>;
dropzones: Array<DropZoneByOaci>;
categories: Array<JumpByModule>;
};
public filteredOptions: {
aeronefs: Observable<Array<AeronefByImat>>;
imats: Observable<Array<AeronefByImat>>;
canopies: Observable<Array<CanopyModelBySize>>;
tailles: Observable<Array<CanopyModelBySize>>;
dropzones: Observable<Array<DropZoneByOaci>>;
oaci: Observable<Array<DropZoneByOaci>>;
categories: Observable<Array<JumpByModule>>;
modules: Observable<Array<JumpByModule>>;
};
constructor(
public dialogRef: MatDialogRef<JumpAddDialogComponent>,
private fb: FormBuilder,
private _aeronefsService: AeronefsService,
private _canopiesService: CanopiesService,
private _dropZonesService: DropZonesService,
private _jumpsService: JumpsService,
@Inject(MAT_DIALOG_DATA) public data: {jump: Jump, lastJump: Jump}
) {
this.inputOptions = { aeronefs: [], canopies: [], dropzones: [], categories: [] };
//this.filteredOptions.aeronefs = this._aeronefsService.getAllByImat();
this.jump = data.jump;
this.lastJump = data.lastJump;
this.nextJump = (this.lastJump.numero + 1);
this.params = {
numeros: this.nextJump.toString(),
date: '',
video: true
};
this.jump.numero = this.nextJump;
const controlsConfig = {
date: ['', Validators.required],
numero: [this.nextJump, Validators.required],
lieu: ['', Validators.required],
oaci: '',
aeronef: ['', Validators.required],
imat: '',
hauteur: ['', Validators.required],
deploiement: '',
voile: '',
taille: ['', Validators.required],
categorie: '',
module: '',
participants: [1, Validators.required],
sautants: [[]],
programme: '',
accessoires: '',
zone: '',
dossier: '',
video: ''
};
this.jumpForm = this.fb.group(controlsConfig);
this.filename = this._computeFilename();
this.filepath = this._computeFilepath();
this._subscriptions.push(
this.jumpForm.controls['module'].valueChanges.subscribe(() => {
this.filepath = this._computeFilepath();
}),
this.jumpForm.controls['numero'].valueChanges.subscribe(() => {
this.filename = this._computeFilename();
})
);
/*
this.filteredAeronefOptions = this.jumpForm.controls['aeronef'].valueChanges.pipe(
startWith(''),
map(value => {
const aeronef = typeof value === 'string' ? value : '';
return aeronef ? this._filterString(aeronef as string, this.aeronefOptions) : this.aeronefOptions.slice();
}),
);
this.filteredImatOptions = this.jumpForm.controls['imat'].valueChanges.pipe(
startWith(''),
map(value => {
const imat = typeof value === 'string' ? value : value?.imat;
return imat ? this._filterImat(imat as string) : this.imatOptions.slice();
}),
);
//this.filteredAeronefOptions = this._aeronefsService.getAllByImat();
*/
this.filteredOptions = {
aeronefs: this.jumpForm.controls['aeronef'].valueChanges.pipe(
startWith(''),
map(value => {
const aeronef = typeof value === 'string' ? value : value?.aeronef;
return aeronef ? this._filterImat(aeronef as string) : this.inputOptions.aeronefs.slice();
}),
),
imats: this.jumpForm.controls['imat'].valueChanges.pipe(
startWith(''),
map(value => {
const imat = typeof value === 'string' ? value : value?.imat;
return imat ? this._filterImat(imat as string) : this.inputOptions.aeronefs.slice();
}),
),
canopies: this.jumpForm.controls['voile'].valueChanges.pipe(
startWith(''),
map(value => {
const voile = typeof value === 'string' ? value : value?.voile;
return voile ? this._filterTaille(voile as string) : this.inputOptions.canopies.slice();
}),
),
tailles: this.jumpForm.controls['taille'].valueChanges.pipe(
startWith(''),
map(value => {
const taille = typeof value === 'string' ? value : value?.taille.toString();
return taille ? this._filterTaille(taille as string) : this.inputOptions.canopies.slice();
}),
),
dropzones: this.jumpForm.controls['lieu'].valueChanges.pipe(
startWith(''),
map(value => {
const lieu = typeof value === 'string' ? value : value?.lieu;
return lieu ? this._filterOaci(lieu as string) : this.inputOptions.dropzones.slice();
}),
),
oaci: this.jumpForm.controls['oaci'].valueChanges.pipe(
startWith(''),
map(value => {
const oaci = typeof value === 'string' ? value : value?.oaci;
return oaci ? this._filterOaci(oaci as string) : this.inputOptions.dropzones.slice();
}),
),
categories: this.jumpForm.controls['categorie'].valueChanges.pipe(
startWith(''),
map(value => {
const category = typeof value === 'string' ? value : value?.categorie;
return category ? this._filterModule(category as string) : this.inputOptions.categories.slice();
}),
),
modules: this.jumpForm.controls['module'].valueChanges.pipe(
startWith(''),
map(value => {
const module = typeof value === 'string' ? value : value?.module;
return module ? this._filterModule(module as string) : this.inputOptions.categories.slice();
}),
)
};
this._loadAeronefByImat();
this._loadCanopyModelBySize();
this._loadDropZoneByOaci();
this._loadJumpByModule();
}
ngOnDestroy() {
this._subscriptions.forEach((sub: Subscription) => {
sub.unsubscribe();
});
this._aeronefByImat.unsubscribe();
this._canopyModelBySize.unsubscribe();
this._dropZoneByOaci.unsubscribe();
this._jumpByModule.unsubscribe();
}
public onDateChange(): void {
this.filename = this._computeFilename();
}
private _computeFilename(numero?: string): string {
let file: string = '';
const timestamp: number = Date.parse(this.jumpForm.controls['date'].value);
if (isNaN(timestamp) == false) {
const date: Date = new Date(timestamp);
file += `${(date.getFullYear() + '').padStart(2, '0')}-${((date.getMonth() + 1) + '').padStart(2, '0')}-${(date.getDate() + '').padStart(2, '0')}_`;
}
if (numero !== undefined) {
numero = (numero + '').padStart(5, '0');
} else {
numero = (this.jumpForm.controls['numero'].value + '').padStart(5, '0');
}
file += `S${numero}.mp4`;
this.jumpForm.controls['video'].setValue(file);
return file;
}
private _computeFilepath(): string {
let path: string = '/Volumes/Storage/Skydive/Videos/';
if (this.jumpForm.controls['module'].value != '') {
//path += `${this.jumpForm.controls['module'].value}/`;
path += typeof this.jumpForm.controls['module'].value === 'string' ? `${this.jumpForm.controls['module'].value}/` : `${this.jumpForm.controls['module'].value?.module}/`;
}
this.jumpForm.controls['dossier'].setValue(path);
return path;
}
private _isLastForAllSections() {
return (
this.useLast.aeronef
&& this.useLast.altitude
&& this.useLast.discipline
&& this.useLast.divers
&& this.useLast.dropzone
&& this.useLast.groupe
&& this.useLast.programme
&& this.useLast.voile
);
}
private _filterImat(imat: string): Array<AeronefByImat> {
const filterValue = imat.toLowerCase();
return this.inputOptions.aeronefs.filter(option => (option.aeronef.toLowerCase().includes(filterValue) || option.imat.toLowerCase().includes(filterValue)));
}
private _filterModule(module: string): Array<JumpByModule> {
const filterValue = module.toLowerCase();
return this.inputOptions.categories.filter(option => (option.categorie.toLowerCase().includes(filterValue) || option.module.toLowerCase().includes(filterValue)));
}
private _filterOaci(oaci: string): Array<DropZoneByOaci> {
const filterValue = oaci.toLowerCase();
return this.inputOptions.dropzones.filter(option => (option.lieu.toLowerCase().includes(filterValue) || option.oaci.toLowerCase().includes(filterValue)));
}
private _filterTaille(taille: string): Array<CanopyModelBySize> {
const filterValue = taille.toLowerCase();
return this.inputOptions.canopies.filter(option => (option.voile.toLowerCase().includes(filterValue) || option.taille.toString().includes(filterValue)));
}
private _loadAeronefByImat(): void {
const aeronefs$ = this._aeronefsService.getAllByImat();
const subscription: Subscription = aeronefs$.subscribe((aggregate: Array<AeronefByImat>) => {
/*
this.inputOptions.imats = aggregate.map((row: AeronefByImat) => {
if (this.inputOptions.aeronefs.indexOf(row.aeronef) === -1) {
this.inputOptions.aeronefs.push(row.aeronef);
}
return row;
});
*/
this.inputOptions.aeronefs = aggregate;
});
this._subscriptions.push(subscription);
}
private _loadCanopyModelBySize(): void {
const canopies$ = this._canopiesService.getAllBySizeByModel();
const subscription: Subscription = canopies$.subscribe((aggregate: Array<CanopyModelBySize>) => {
/*
this.inputOptions.tailles = aggregate.map((row: CanopyModelBySize) => {
if (this.inputOptions.canopies.indexOf(row.voile) === -1) {
this.inputOptions.canopies.push(row.voile);
}
return row;
});
*/
this.inputOptions.canopies = aggregate;
});
this._subscriptions.push(subscription);
}
private _loadDropZoneByOaci(): void {
const dropzones$ = this._dropZonesService.getAllByOaci();
const subscription: Subscription = dropzones$.subscribe((aggregate: Array<DropZoneByOaci>) => {
/*
this.inputOptions.oaci = aggregate.map((row: DropZoneByOaci) => {
if (this.inputOptions.dropzones.indexOf(row.lieu) === -1) {
this.inputOptions.dropzones.push(row.lieu);
}
return row;
});
*/
this.inputOptions.dropzones = aggregate;
});
this._subscriptions.push(subscription);
}
private _loadJumpByModule(): void {
const jumps$ = this._jumpsService.getAllByModule();
const subscription: Subscription = jumps$.subscribe((aggregate: Array<JumpByModule>) => {
this.inputOptions.categories = aggregate;
});
this._subscriptions.push(subscription);
}
private _setAllLast(checked: boolean) {
this.useLast.all = checked;
this.useLast.aeronef = checked;
this.useLast.altitude = checked;
this.useLast.discipline = checked;
this.useLast.divers = checked;
this.useLast.dropzone = checked;
this.useLast.groupe = checked;
this.useLast.programme = checked;
this.useLast.voile = checked;
}
addSautant(): void {
this.useLast.groupe = false;
this.useLast.all = this._isLastForAllSections();
const tag = this.tagField.value;
if (tag != null && tag.trim() !== '') {
if (tag.includes(',') === true) {
const tags = tag.split(',');
tags.forEach(sautant => {
if (this.jump.sautants.indexOf(sautant.trim()) < 0) {
this.jump.sautants.push(sautant.trim());
}
});
} else if (this.jump.sautants.indexOf(tag) < 0) {
this.jump.sautants.push(tag);
}
}
this.tagField.reset('');
const participants = (this.jump.sautants.length + 1);
this.jumpForm.controls['participants'].setValue(participants);
}
closeDialog(): void {
this._setAllLast(false);
//this.jump = <Jump>{};
//this.jump.numero = this.nextJump;
this.dialogRef.close();
}
displayAeronefFn(aeronef: AeronefByImat): string {
let res = '';
if (aeronef) {
if (typeof aeronef === 'string') {
res = aeronef;
} else {
res = aeronef.aeronef ? aeronef.aeronef : '';
}
}
return res;
}
displayImatFn(aeronef: AeronefByImat): string {
let res = '';
if (aeronef) {
if (typeof aeronef === 'string') {
res = aeronef;
} else {
res = aeronef.imat ? aeronef.imat : '';
}
}
return res;
}
displayLieuFn(dropzone: DropZoneByOaci): string {
//return dropzone && dropzone.oaci ? `${dropzone.lieu} - ${dropzone.oaci}` : '';
let res = '';
if (dropzone) {
if (typeof dropzone === 'string') {
res = dropzone;
} else {
res = dropzone.lieu ? dropzone.lieu : '';
}
}
//return dropzone && dropzone.lieu ? dropzone.lieu : '';
return res;
}
displayOaciFn(dropzone: DropZoneByOaci): string {
//return dropzone && dropzone.oaci ? `${dropzone.lieu} - ${dropzone.oaci}` : '';
let res = '';
if (dropzone) {
if (typeof dropzone === 'string') {
res = dropzone;
} else {
res = dropzone.oaci ? dropzone.oaci : '';
}
}
//return dropzone && dropzone.oaci ? dropzone.oaci : '';
return res;
}
displayTailleFn(canopy: CanopyModelBySize): string {
let res = '';
if (canopy) {
if (typeof canopy === 'string') {
res = canopy;
} else {
res = canopy.taille ? canopy.taille.toString() : '';
}
}
return res;
}
displayVoileFn(canopy: CanopyModelBySize): string {
let res = '';
if (canopy) {
if (typeof canopy === 'string') {
res = canopy;
} else {
res = canopy.voile ? canopy.voile : '';
}
}
return res;
}
displayCategorieFn(jump: JumpByModule): string {
let res = '';
if (jump) {
if (typeof jump === 'string') {
res = jump;
} else {
res = jump.categorie ? jump.categorie : '';
}
}
return res;
}
displayModuleFn(jump: JumpByModule): string {
let res = '';
if (jump) {
if (typeof jump === 'string') {
res = jump;
} else {
res = jump.module ? jump.module : '';
}
}
return res;
}
getErrorMessage(name: string): string {
if (this.jumpForm.controls[name].errors !== null) {
return 'Ce champ est requis.';
}
return '';
}
onSelAeronef(aeronef: AeronefByImat) {
if (aeronef && aeronef.aeronef) {
//this.jumpForm.controls['imat'].setValue(aeronef.imat);
this.jumpForm.controls['imat'].setValue(aeronef);
}
}
onSelImat(aeronef: AeronefByImat) {
if (aeronef && aeronef.imat) {
//this.jumpForm.controls['aeronef'].setValue(aeronef.aeronef);
this.jumpForm.controls['aeronef'].setValue(aeronef);
}
}
onSelLieu(dropzone: DropZoneByOaci) {
if (dropzone && dropzone.oaci) {
//this.jumpForm.controls['oaci'].setValue(dropzone.oaci);
this.jumpForm.controls['oaci'].setValue(dropzone);
}
}
onSelOaci(dropzone: DropZoneByOaci) {
if (dropzone && dropzone.lieu) {
//this.jumpForm.controls['lieu'].setValue(dropzone.lieu);
this.jumpForm.controls['lieu'].setValue(dropzone);
}
}
onSelTaille(canopy: CanopyModelBySize) {
if (canopy && canopy.voile) {
//this.jumpForm.controls['voile'].setValue(canopy.voile);
this.jumpForm.controls['voile'].setValue(canopy);
}
}
onSelVoile(canopy: CanopyModelBySize) {
if (canopy && canopy.taille) {
//this.jumpForm.controls['taille'].setValue(canopy.taille.toString());
this.jumpForm.controls['taille'].setValue(canopy);
}
}
onSelCategorie(jump: JumpByModule) {
if (jump && jump.categorie) {
this.jumpForm.controls['module'].setValue(jump);
}
}
onSelModule(jump: JumpByModule) {
if (jump && jump.module) {
this.jumpForm.controls['categorie'].setValue(jump);
}
}
removeSautant(index: string): void {
this.useLast.groupe = false;
this.useLast.all = this._isLastForAllSections();
this.jump.sautants = this.jump.sautants.filter((sautant) => sautant !== index);
const participants = (this.jump.sautants.length + 1);
this.jumpForm.controls['participants'].setValue(participants);
}
setLastJump(checked: boolean) {
if (checked) {
this._setAllLast(true);
this.jumpForm.controls['lieu'].setValue(this.lastJump.lieu);
this.jumpForm.controls['oaci'].setValue(this.lastJump.oaci);
this.jumpForm.controls['aeronef'].setValue(this.lastJump.aeronef);
this.jumpForm.controls['imat'].setValue(this.lastJump.imat);
this.jumpForm.controls['hauteur'].setValue(this.lastJump.hauteur);
this.jumpForm.controls['deploiement'].setValue(this.lastJump.deploiement);
this.jumpForm.controls['voile'].setValue(this.lastJump.voile);
this.jumpForm.controls['taille'].setValue(this.lastJump.taille?.toString());
this.jumpForm.controls['categorie'].setValue(this.lastJump.categorie);
this.jumpForm.controls['module'].setValue(this.lastJump.module);
this.jumpForm.controls['participants'].setValue(this.lastJump.participants);
//this.jumpForm.controls['sautants'].setValue(this.lastJump.sautants);
this.jumpForm.controls['programme'].setValue(this.lastJump.programme);
this.jumpForm.controls['accessoires'].setValue(this.lastJump.accessoires);
this.jumpForm.controls['zone'].setValue(this.lastJump.zone);
this.jump.sautants = this.lastJump.sautants;
/*
Object.assign(
this.jump,
{
date: this.lastJump.date,
numero: this.lastJump.numero,
lieu: this.lastJump.lieu,
oaci: this.lastJump.oaci,
aeronef: this.lastJump.aeronef,
imat: this.lastJump.imat,
hauteur: this.lastJump.hauteur,
deploiement: this.lastJump.deploiement,
voile: this.lastJump.voile,
taille: this.lastJump.taille,
categorie: this.lastJump.categorie,
module: this.lastJump.module,
participants: this.lastJump.participants,
sautants: this.lastJump.sautants,
programme: this.lastJump.programme,
accessoires: this.lastJump.accessoires,
zone: this.lastJump.zone,
}
);
*/
this.jump.date = '';
} else {
this._setAllLast(false);
this.jumpForm.controls['lieu'].setValue('');
this.jumpForm.controls['oaci'].setValue('');
this.jumpForm.controls['aeronef'].setValue('');
this.jumpForm.controls['imat'].setValue('');
this.jumpForm.controls['hauteur'].setValue('');
this.jumpForm.controls['deploiement'].setValue('');
this.jumpForm.controls['voile'].setValue('');
this.jumpForm.controls['taille'].setValue('');
this.jumpForm.controls['categorie'].setValue('');
this.jumpForm.controls['module'].setValue('');
this.jumpForm.controls['participants'].setValue(1);
//this.jumpForm.controls['sautants'].setValue('');
this.jumpForm.controls['programme'].setValue('');
this.jumpForm.controls['accessoires'].setValue('');
this.jumpForm.controls['zone'].setValue('');
this.jump.sautants = [];
}
this.jump.numero = this.nextJump;
}
setLastAeronef(checked: boolean = false) {
if (checked) {
this.useLast.aeronef = true;
this.jumpForm.controls['aeronef'].setValue(this.lastJump.aeronef);
this.jumpForm.controls['imat'].setValue(this.lastJump.imat);
} else {
this.useLast.aeronef = false;
this.jumpForm.controls['aeronef'].setValue('');
this.jumpForm.controls['imat'].setValue('');
}
this.useLast.all = this._isLastForAllSections();
}
setLastAltitude(checked: boolean = false) {
if (checked) {
this.useLast.altitude = true;
this.jumpForm.controls['hauteur'].setValue(this.lastJump.hauteur);
this.jumpForm.controls['deploiement'].setValue(this.lastJump.deploiement);
} else {
this.useLast.altitude = false;
this.jumpForm.controls['hauteur'].setValue('');
this.jumpForm.controls['deploiement'].setValue('');
}
this.useLast.all = this._isLastForAllSections();
}
setLastDiscipline(checked: boolean = false) {
if (checked) {
this.useLast.discipline = true;
this.jumpForm.controls['categorie'].setValue(this.lastJump.categorie);
this.jumpForm.controls['module'].setValue(this.lastJump.module);
} else {
this.useLast.discipline = false;
this.jumpForm.controls['categorie'].setValue('');
this.jumpForm.controls['module'].setValue('');
}
this.useLast.all = this._isLastForAllSections();
}
setLastDivers(checked: boolean = false) {
if (checked) {
this.useLast.divers = true;
this.jumpForm.controls['accessoires'].setValue(this.lastJump.accessoires);
this.jumpForm.controls['zone'].setValue(this.lastJump.zone);
} else {
this.useLast.divers = false;
this.jumpForm.controls['accessoires'].setValue('');
this.jumpForm.controls['zone'].setValue('');
}
this.useLast.all = this._isLastForAllSections();
}
setLastDropzone(checked: boolean = false) {
if (checked) {
this.useLast.dropzone = true;
this.jumpForm.controls['lieu'].setValue(this.lastJump.lieu);
this.jumpForm.controls['oaci'].setValue(this.lastJump.oaci);
} else {
this.useLast.dropzone = false;
this.jumpForm.controls['lieu'].setValue('');
this.jumpForm.controls['oaci'].setValue('');
}
this.useLast.all = this._isLastForAllSections();
}
setLastGroupe(checked: boolean = false) {
if (checked) {
this.useLast.groupe = true;
this.jumpForm.controls['participants'].setValue(this.lastJump.participants);
//this.jumpForm.controls['sautants'].setValue(this.lastJump.sautants);
this.jump.sautants = this.lastJump.sautants;
} else {
this.useLast.groupe = false;
this.jumpForm.controls['participants'].setValue(1);
//this.jumpForm.controls['sautants'].setValue('');
this.jump.sautants = [];
}
this.useLast.all = this._isLastForAllSections();
}
setLastProgramme(checked: boolean = false) {
if (checked) {
this.useLast.programme = true;
this.jumpForm.controls['programme'].setValue(this.lastJump.participants);
} else {
this.useLast.programme = false;
this.jumpForm.controls['programme'].setValue('');
}
this.useLast.all = this._isLastForAllSections();
}
setLastVoile(checked: boolean = false) {
if (checked) {
this.useLast.voile = true;
this.jumpForm.controls['voile'].setValue(this.lastJump.voile);
this.jumpForm.controls['taille'].setValue(this.lastJump.taille?.toString());
} else {
this.useLast.voile = false;
this.jumpForm.controls['voile'].setValue('');
this.jumpForm.controls['taille'].setValue('');
}
this.useLast.all = this._isLastForAllSections();
}
submitForm(): void {
/*
* Création d'un tableau de saut contenant de 1 à n saut en fonction
* de la valeur du champ numero du formulaire jumpForm
*/
const jumps: Array<Jump> = [];
const jump: Jump = {} as Jump;
const numeros: Array<number> = [];
let date: Date;
/* Controle de la validité du champ date et conversion au format ISO */
const timestamp: number = Date.parse(this.jumpForm.controls['date'].value);
if (isNaN(timestamp) == false) {
date = new Date(timestamp);
} else {
date = new Date();
}
let value = this.jumpForm.controls['numero'].value;
if (typeof value !== 'string') {
value = value.toString();
}
if (isNaN(+value) || value.split('+').length == 2) {
/*
* 'numero' n'est pas un entier , il s'agit donc d'une plage, d'une quantité ou de plusieurs valeur
* decomposer la valeur 'numero' et ajouter chaque sauts au tableau retourné par la fenêtre dialog
*/
let jumpNum: number = this.lastJump.numero;
const elements: string[] = this.jumpForm.controls['numero'].value.split(';');
elements.forEach((element: string) => {
if (element == '') {
jumpNum ++;
element = jumpNum.toString();
}
const values: string[] = element.split('-');
if (values.length == 2) {
const start: number = parseInt(values[0]);
const stop: number = parseInt(values[1]);
const diff: number = (stop - start);
/* Ajout de saut par plage */
for (let index = 0; index <= diff; index++) {
jumpNum = (start + index);
numeros.push(jumpNum);
}
} else {
const quantity: string[] = values[0].split('+');
if (quantity.length == 2) {
/* Ajout de sauts par quantité */
//const start: number = jumpNum;
for (let index = 1; index <= parseInt(quantity[1]); index++) {
jumpNum ++;
numeros.push(jumpNum);
}
} else {
/* Ajout de sauts par numero */
jumpNum = parseInt(quantity[0]);
numeros.push(jumpNum);
}
}
});
} else {
/* 'numero' est un entier */
numeros.push(this.jumpForm.controls['numero'].value);
}
/* Valeurs communes aux 1 à n saut(s) à ajouter */
jump.date = date.toISOString();
jump.lieu = typeof this.jumpForm.controls['lieu'].value === 'string' ? this.jumpForm.controls['lieu'].value : this.jumpForm.controls['lieu'].value.lieu;
jump.oaci = typeof this.jumpForm.controls['oaci'].value === 'string' ? this.jumpForm.controls['oaci'].value : this.jumpForm.controls['oaci'].value.oaci;
jump.aeronef = typeof this.jumpForm.controls['aeronef'].value === 'string' ? this.jumpForm.controls['aeronef'].value : this.jumpForm.controls['aeronef'].value.aeronef;
jump.imat = typeof this.jumpForm.controls['imat'].value === 'string' ? this.jumpForm.controls['imat'].value : this.jumpForm.controls['imat'].value.imat;
jump.voile = typeof this.jumpForm.controls['voile'].value === 'string' ? this.jumpForm.controls['voile'].value : this.jumpForm.controls['voile'].value.voile;
jump.taille = typeof this.jumpForm.controls['taille'].value === 'string' ? this.jumpForm.controls['taille'].value : this.jumpForm.controls['taille'].value.taille;
jump.categorie = typeof this.jumpForm.controls['categorie'].value === 'string' ? this.jumpForm.controls['categorie'].value : this.jumpForm.controls['categorie'].value.categorie;
jump.module = typeof this.jumpForm.controls['module'].value === 'string' ? this.jumpForm.controls['module'].value : this.jumpForm.controls['module'].value.module;
jump.hauteur = this.jumpForm.controls['hauteur'].value;
jump.deploiement = this.jumpForm.controls['deploiement'].value;
jump.participants = this.jumpForm.controls['participants'].value;
jump.sautants = this.jump.sautants;
//jump.sautants = this.jumpForm.controls['sautants'].value;
jump.programme = this.jumpForm.controls['programme'].value;
jump.accessoires = this.jumpForm.controls['accessoires'].value;
jump.zone = this.jumpForm.controls['zone'].value;
//jump.dossier = this.jumpForm.controls['dossier'].value;
//jump.video = this.jumpForm.controls['video'].value;
/* Ajout du saut au tableau à retourner */
numeros.forEach((numero: number) => {
const item: Jump = {} as Jump;
Object.assign(item, jump);
item.numero = numero;
if (this.jumpForm.controls['dossier'].value !== '' && this.jumpForm.controls['video'].value !== '') {
item.dossier = this._computeFilepath();
item.video = this._computeFilename(numero.toString());
const file: JumpFile = {
name: item.video,
path: item.dossier,
type: JumpFileType.VIDEO
} as JumpFile;
this._subscriptions.push(
this._jumpsService.saveFile(item.slug, file)
.pipe(take(1))
.subscribe({
next: (file) => {
item.files.push(file);
console.log(`Le fichier ${file.name} a été ajouté au saut n°${item.numero} !`, 'OK');
},
error: (err) => {
this.errors = err;
}
})
);
}
jumps.push(item);
//console.log(item);
});
//console.log(jumps);
/* Reset all 'last jump values' booleans */
this._setAllLast(false);
/* close the dialog with result */
this.dialogRef.close(jumps);
/*
// Reset all 'last jump values' booleans
this._setAllLast(false);
// update the model
this.updateJump(this.jumpForm.value);
// close the dialog with result
this.dialogRef.close(this.jump);
*/
}
updateJump(values: NonNullable<unknown>): void {
Object.assign(this.jump, values);
}
}
@@ -0,0 +1,64 @@
<h2 mat-dialog-title class="bg-danger border-bottom border-light-subtle d-flex mb-3 pt-3 pb-2">
Supprimer le saut n°{{jump.numero}}<span class="flex-spacer"></span>
<mat-icon fontIcon="close" class="pointer" (click)="closeDialog()"></mat-icon>
</h2>
<mat-dialog-content>
<div class="container">
<div class="mt-2 mb-4 text-center text-danger border-danger alert alert-danger">
<p class="fs-6 m-0">
Veuillez confirmer la suppression de ce saut.<br />
<span class="fw-semibold">Attention</span>, cette <span class="fw-semibold">action</span> est <span class="fw-semibold">irréversible</span>.
</p>
</div>
<h3 class="border-bottom border-light-subtle pb-2 mb-3">Données du saut :</h3>
<div class="row mb-3">
<div class="col-lg-6 col-md-12">
<dl class="dl-horizontal">
<dt class="text-accent fw-normal">numero:</dt>
<dd>{{jump.numero}}</dd>
<dt class="text-accent fw-normal">lieu:</dt>
<dd>{{jump.lieu}}</dd>
<dt class="text-accent fw-normal">aeronef:</dt>
<dd>{{jump.aeronef}}</dd>
<dt class="text-accent fw-normal">voile:</dt>
<dd>{{jump.voile}}</dd>
<dt class="text-accent fw-normal">hauteur:</dt>
<dd>{{jump.hauteur}} m</dd>
<dt class="text-accent fw-normal">categorie:</dt>
<dd>{{jump.categorie}}</dd>
<dt class="text-accent fw-normal">accessoires:</dt>
<dd>{{jump.accessoires}}</dd>
<dt class="text-accent fw-normal">dossier:</dt>
<dd>{{jump.dossier}}</dd>
<dt class="text-accent fw-normal">programme:</dt>
<dd>{{jump.programme}}</dd>
</dl>
</div>
<div class="col-lg-6 col-md-12">
<dl class="dl-horizontal">
<dt class="text-accent fw-normal">date:</dt>
<dd>{{jump.date | date: 'mediumDate'}}</dd>
<dt class="text-accent fw-normal">oaci:</dt>
<dd>{{jump.oaci}}</dd>
<dt class="text-accent fw-normal">imat:</dt>
<dd>{{jump.imat}}</dd>
<dt class="text-accent fw-normal">taille:</dt>
<dd>{{jump.taille}} ft<sup>2</sup></dd>
<dt class="text-accent fw-normal">participants:</dt>
<dd>{{jump.participants}}</dd>
<dt class="text-accent fw-normal">module:</dt>
<dd>{{jump.module}}</dd>
<dt class="text-accent fw-normal">zone:</dt>
<dd>{{jump.zone}}</dd>
<dt class="text-accent fw-normal">video:</dt>
<dd>{{jump.video}}</dd>
</dl>
</div>
</div>
</div>
</mat-dialog-content>
<mat-dialog-actions>
<span class="flex-spacer"></span>
<button mat-button (click)="closeDialog()" class="me-1">Annuler</button>
<button mat-button color="accent" [mat-dialog-close]="jump" cdkFocusInitial>Supprimer</button>
</mat-dialog-actions>
@@ -0,0 +1,27 @@
import { Component, Inject } from '@angular/core';
import { DatePipe } from '@angular/common';
import { MatButtonModule } from '@angular/material/button';
import { MatDialogRef, MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';
import { MatIconModule } from '@angular/material/icon';
import { Jump } from '@models';
@Component({
selector: 'app-jump-delete-dialog',
templateUrl: 'jump-delete.dialog.html',
standalone: true,
imports: [
DatePipe,
MatDialogModule, MatButtonModule, MatIconModule
]
})
export class JumpDeleteDialogComponent {
constructor(
public dialogRef: MatDialogRef<JumpDeleteDialogComponent>,
@Inject(MAT_DIALOG_DATA) public jump: Jump
) { }
closeDialog(): void {
this.dialogRef.close();
}
}
@@ -0,0 +1,219 @@
<h2 mat-dialog-title class="bg-primary border-bottom border-light-subtle d-flex mb-3 pt-3 pb-2">
Modifier un saut<span class="flex-spacer"></span>
<mat-icon fontIcon="close" class="pointer" (click)="closeDialog()"></mat-icon>
</h2>
<mat-dialog-content>
<div class="container">
<form class="row" [formGroup]="jumpForm" (ngSubmit)="submitForm()">
<div class="col-xs-12">
<mat-form-field appearance="outline" class="mt-2 me-2" color="primary">
<mat-label>Numéro du saut</mat-label>
<input matInput formControlName="numero">
</mat-form-field>
<mat-form-field appearance="outline" class="mt-2 me-3" color="primary">
<mat-label>Choisissez une date</mat-label>
<input matInput [matDatepicker]="datepicker" formControlName="date" />
<mat-hint>MM/DD/YYYY</mat-hint>
<mat-datepicker-toggle matIconSuffix [for]="datepicker"></mat-datepicker-toggle>
<mat-datepicker #datepicker>
<mat-datepicker-actions>
<button mat-button matDatepickerCancel>Annuler</button>
<button mat-raised-button color="primary" matDatepickerApply>Appliquer</button>
</mat-datepicker-actions>
</mat-datepicker>
</mat-form-field>
</div>
<div class="col-lg-6 col-md-12">
<h4 class="border-bottom border-primary pb-1 mb-3">Dropzone</h4>
<mat-form-field appearance="fill" class="me-2">
<mat-label>Lieu</mat-label>
<input matInput formControlName="lieu" [matAutocomplete]="autoDropzone">
<mat-autocomplete #autoDropzone="matAutocomplete" [displayWith]="displayLieuFn">
@for (option of filteredOptions.dropzones | async; track option) {
<mat-option [value]="option" (onSelectionChange)="onSelLieu(option)">{{option.lieu}} - {{option.oaci}}</mat-option>
}
</mat-autocomplete>
@if (this.jumpForm.controls['lieu'].status === 'INVALID') {
<mat-error>{{getErrorMessage('lieu')}}</mat-error>
}
</mat-form-field>
<mat-form-field appearance="fill" class="me-2">
<mat-label>Oaci</mat-label>
<input matInput formControlName="oaci" [matAutocomplete]="autoOaci">
<mat-autocomplete #autoOaci="matAutocomplete" [displayWith]="displayOaciFn">
@for (option of filteredOptions.oaci | async; track option) {
<mat-option [value]="option" (onSelectionChange)="onSelOaci(option)">{{option.lieu}} - {{option.oaci}}</mat-option>
}
</mat-autocomplete>
@if (this.jumpForm.controls['oaci'].status === 'INVALID') {
<mat-error>{{getErrorMessage('oaci')}}</mat-error>
}
</mat-form-field>
<h4 class="border-bottom border-primary pb-1 mb-3">Aéronef</h4>
<mat-form-field appearance="fill" class="me-2">
<mat-label>Aéronef</mat-label>
<input matInput formControlName="aeronef" [matAutocomplete]="autoAeronef">
<mat-autocomplete #autoAeronef="matAutocomplete" [displayWith]="displayAeronefFn">
@for (option of filteredOptions.aeronefs | async; track option) {
<mat-option [value]="option" (onSelectionChange)="onSelAeronef(option)">{{option.aeronef}} - {{option.imat}}</mat-option>
}
</mat-autocomplete>
@if (this.jumpForm.controls['aeronef'].status === 'INVALID') {
<mat-error>{{getErrorMessage('aeronef')}}</mat-error>
}
</mat-form-field>
<mat-form-field appearance="fill" class="me-2">
<mat-label>Imatriculation</mat-label>
<input matInput formControlName="imat" [matAutocomplete]="autoImat">
<mat-autocomplete #autoImat="matAutocomplete" [displayWith]="displayImatFn">
@for (option of filteredOptions.imats | async; track option) {
<mat-option [value]="option" (onSelectionChange)="onSelImat(option)">{{option.aeronef}} - {{option.imat}}</mat-option>
}
</mat-autocomplete>
@if (this.jumpForm.controls['imat'].status === 'INVALID') {
<mat-error>{{getErrorMessage('imat')}}</mat-error>
}
</mat-form-field>
<h4 class="border-bottom border-primary pb-1 mb-3">Voile</h4>
<mat-form-field appearance="fill" class="me-2">
<mat-label>Modèle</mat-label>
<input matInput formControlName="voile" [matAutocomplete]="autoCanopy">
<mat-autocomplete #autoCanopy="matAutocomplete" [displayWith]="displayVoileFn">
@for (option of filteredOptions.canopies | async; track option) {
<mat-option [value]="option" (onSelectionChange)="onSelVoile(option)">{{option.voile}} - {{option.taille}}</mat-option>
}
</mat-autocomplete>
@if (this.jumpForm.controls['voile'].status === 'INVALID') {
<mat-error>{{getErrorMessage('voile')}}</mat-error>
}
</mat-form-field>
<mat-form-field appearance="fill" class="me-2">
<mat-label>Taille en ft<sup>2</sup></mat-label>
<input matInput formControlName="taille" [matAutocomplete]="autoTaille">
<mat-autocomplete #autoTaille="matAutocomplete" [displayWith]="displayTailleFn">
@for (option of filteredOptions.tailles | async; track option) {
<mat-option [value]="option" (onSelectionChange)="onSelTaille(option)">{{option.voile}} - {{option.taille}}</mat-option>
}
</mat-autocomplete>
<span matTextSuffix>ft<sup>2</sup></span>
@if (this.jumpForm.controls['taille'].status === 'INVALID') {
<mat-error>{{getErrorMessage('taille')}}</mat-error>
}
</mat-form-field>
</div>
<div class="col-lg-6 col-md-12">
<h4 class="border-bottom border-primary pb-1 mb-3">Altitude</h4>
<mat-form-field appearance="fill" class="me-2">
<mat-label>Hauteur de largage</mat-label>
<input matInput formControlName="hauteur" type="number" min="0" step="1">
@if (this.jumpForm.controls['hauteur'].status === 'INVALID') {
<mat-error>{{getErrorMessage('hauteur')}}</mat-error>
}
</mat-form-field>
<mat-form-field appearance="fill" class="me-2">
<mat-label>Hauteur de déploiement</mat-label>
<input matInput formControlName="deploiement" type="number" min="0" step="1">
@if (this.jumpForm.controls['deploiement'].status === 'INVALID') {
<mat-error>{{getErrorMessage('deploiement')}}</mat-error>
}
</mat-form-field>
<h4 class="border-bottom border-primary pb-1 mb-3">Discipline</h4>
<mat-form-field appearance="fill" class="me-2">
<mat-label>Categorie</mat-label>
<input matInput formControlName="categorie" [matAutocomplete]="autoCategorie">
<mat-autocomplete #autoCategorie="matAutocomplete" [displayWith]="displayCategorieFn">
@for (option of filteredOptions.categories | async; track option) {
<mat-option [value]="option" (onSelectionChange)="onSelCategorie(option)">{{option.categorie}}{{option.categorie && option.module ? ' - ' : ''}}{{option.module}}</mat-option>
}
</mat-autocomplete>
@if (this.jumpForm.controls['categorie'].status === 'INVALID') {
<mat-error>{{getErrorMessage('categorie')}}</mat-error>
}
</mat-form-field>
<mat-form-field appearance="fill" class="me-2">
<mat-label>Module</mat-label>
<input matInput formControlName="module" [matAutocomplete]="autoModule">
<mat-autocomplete #autoModule="matAutocomplete" [displayWith]="displayModuleFn">
@for (option of filteredOptions.modules | async; track option) {
<mat-option [value]="option" (onSelectionChange)="onSelModule(option)">{{option.categorie}}{{option.categorie && option.module ? ' - ' : ''}}{{option.module}}</mat-option>
}
</mat-autocomplete>
@if (this.jumpForm.controls['module'].status === 'INVALID') {
<mat-error>{{getErrorMessage('module')}}</mat-error>
}
</mat-form-field>
<h4 class="border-bottom border-primary pb-1 mb-3">Divers</h4>
<mat-form-field appearance="fill" class="me-2">
<mat-label>Accessoires</mat-label>
<input matInput formControlName="accessoires">
@if (this.jumpForm.controls['accessoires'].status === 'INVALID') {
<mat-error>{{getErrorMessage('accessoires')}}</mat-error>
}
</mat-form-field>
<mat-form-field appearance="fill" class="me-2">
<mat-label>Zone</mat-label>
<input matInput formControlName="zone">
@if (this.jumpForm.controls['zone'].status === 'INVALID') {
<mat-error>{{getErrorMessage('zone')}}</mat-error>
}
</mat-form-field>
</div>
<div class="col-lg-6 col-md-12">
<h4 class="border-bottom border-primary pb-1 mb-3">Groupe</h4>
<mat-form-field appearance="fill" class="me-2">
<mat-label>Sautants</mat-label>
<input matInput placeholder="Ajouter un sautant" [formControl]="tagField" (keyup.enter)="addSautant()">
</mat-form-field>
<mat-form-field appearance="fill" class="me-2">
<mat-label>Nombre de participants</mat-label>
<input matInput formControlName="participants" type="number" min="0" step="1">
@if (this.jumpForm.controls['participants'].status === 'INVALID') {
<mat-error>{{getErrorMessage('participants')}}</mat-error>
}
</mat-form-field>
<div class="mb-2">
@for (sautant of jump.sautants; track sautant) {
<span class="badge rounded-pill bg-primary mb-2 me-1 pe-3 py-1 fw-normal text-dark">
<mat-icon class="align-middle pointer me-1" fontIcon="close" (click)="removeSautant(sautant)"></mat-icon>
{{ sautant }}
</span>
}
<span class="badge rounded-pill bg-accent mb-2 me-1 pe-3 py-1 fw-normal text-dark">
<mat-icon class="align-middle pointer me-1" fontIcon="person"></mat-icon>
{{ jump.author.username }}
</span>
</div>
</div>
<div class="col-lg-6 col-md-12">
<h4 class="border-bottom border-primary pb-1 mb-3">Programme</h4>
<mat-form-field appearance="fill" class="me-2 w-100">
<mat-label>Programme</mat-label>
<textarea matInput formControlName="programme"></textarea>
@if (this.jumpForm.controls['programme'].status === 'INVALID') {
<mat-error>{{getErrorMessage('programme')}}</mat-error>
}
</mat-form-field>
<h4 class="border-bottom border-primary pb-1 mb-3">Vidéo</h4>
<mat-form-field appearance="fill" class="me-2">
<mat-label>Dossier</mat-label>
<input matInput formControlName="dossier">
@if (this.jumpForm.controls['dossier'].status === 'INVALID') {
<mat-error>{{getErrorMessage('dossier')}}</mat-error>
}
</mat-form-field>
<mat-form-field appearance="fill" class="me-2">
<mat-label>Video</mat-label>
<input matInput formControlName="video">
@if (this.jumpForm.controls['video'].status === 'INVALID') {
<mat-error>{{getErrorMessage('video')}}</mat-error>
}
</mat-form-field>
</div>
</form>
</div>
</mat-dialog-content>
<mat-dialog-actions>
<span class="flex-spacer"></span>
<button mat-button (click)="closeDialog()" class="me-1">Annuler</button>
<button mat-button color="primary" [disabled]="!jumpForm.valid" (click)="submitForm()" cdkFocusInitial>Enregistrer</button>
</mat-dialog-actions>
@@ -0,0 +1,407 @@
import { Component, Inject, OnDestroy } from '@angular/core';
import { AsyncPipe, DatePipe } from '@angular/common';
import { FormBuilder, FormControl, FormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms';
import { MatAutocompleteModule } from '@angular/material/autocomplete';
import { MatButtonModule } from '@angular/material/button';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatDatepickerModule } from '@angular/material/datepicker';
import { MatDialogRef, MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatOptionModule } from '@angular/material/core';
import { MatSelectModule } from '@angular/material/select';
import { Observable, Subscription } from 'rxjs';
import { map, startWith } from 'rxjs/operators';
import { AeronefByImat, CanopyModelBySize, DropZoneByOaci, Jump, JumpByModule } from '@models';
import { AeronefsService, CanopiesService, DropZonesService, JumpsService } from '@services';
@Component({
selector: 'app-jump-edit-dialog',
templateUrl: 'jump-edit.dialog.html',
standalone: true,
imports: [
AsyncPipe,
DatePipe,
FormsModule,
ReactiveFormsModule,
MatAutocompleteModule,
MatButtonModule,
MatCheckboxModule,
MatDatepickerModule,
MatDialogModule,
MatFormFieldModule,
MatIconModule,
MatInputModule,
MatSelectModule,
MatOptionModule
]
})
export class JumpEditDialogComponent implements OnDestroy {
private _aeronefByImat: Subscription = new Subscription();
private _canopyModelBySize: Subscription = new Subscription();
private _dropZoneByOaci: Subscription = new Subscription();
private _jumpByModule: Subscription = new Subscription();
public tagField = new FormControl<string>('', { nonNullable: true});
public jumpForm: FormGroup;
public inputOptions: {
aeronefs: Array<AeronefByImat>;
canopies: Array<CanopyModelBySize>;
dropzones: Array<DropZoneByOaci>;
categories: Array<JumpByModule>;
};
public filteredOptions: {
aeronefs: Observable<Array<AeronefByImat>>;
imats: Observable<Array<AeronefByImat>>;
canopies: Observable<Array<CanopyModelBySize>>;
tailles: Observable<Array<CanopyModelBySize>>;
dropzones: Observable<Array<DropZoneByOaci>>;
oaci: Observable<Array<DropZoneByOaci>>;
categories: Observable<Array<JumpByModule>>;
modules: Observable<Array<JumpByModule>>;
};
constructor(
public dialogRef: MatDialogRef<JumpEditDialogComponent>,
private fb: FormBuilder,
private _aeronefsService: AeronefsService,
private _canopiesService: CanopiesService,
private _dropZonesService: DropZonesService,
private _jumpsService: JumpsService,
@Inject(MAT_DIALOG_DATA) public jump: Jump
) {
this.inputOptions = { aeronefs: [], canopies: [], dropzones: [], categories: [] };
const controlsConfig = {
slug: [this.jump.slug, Validators.required],
date: [this.jump.date, Validators.required],
numero: [this.jump.numero, Validators.required],
lieu: [this.jump.lieu, Validators.required],
oaci: this.jump.oaci,
aeronef: [this.jump.aeronef, Validators.required],
imat: this.jump.imat,
hauteur: [this.jump.hauteur, Validators.required],
deploiement: this.jump.deploiement,
voile: this.jump.voile,
taille: [this.jump.taille?.toString(), Validators.required],
categorie: this.jump.categorie,
module: this.jump.module,
participants: [this.jump.participants, Validators.required],
programme: this.jump.programme,
accessoires: this.jump.accessoires,
zone: this.jump.zone,
dossier: this.jump.dossier,
video: this.jump.video
};
this.jumpForm = this.fb.group(controlsConfig);
this.filteredOptions = {
aeronefs: this.jumpForm.controls['aeronef'].valueChanges.pipe(
startWith(''),
map(value => {
const aeronef = typeof value === 'string' ? value : value?.aeronef;
return aeronef ? this._filterImat(aeronef as string) : this.inputOptions.aeronefs.slice();
}),
),
imats: this.jumpForm.controls['imat'].valueChanges.pipe(
startWith(''),
map(value => {
const imat = typeof value === 'string' ? value : value?.imat;
return imat ? this._filterImat(imat as string) : this.inputOptions.aeronefs.slice();
}),
),
canopies: this.jumpForm.controls['voile'].valueChanges.pipe(
startWith(''),
map(value => {
const voile = typeof value === 'string' ? value : value?.voile;
return voile ? this._filterTaille(voile as string) : this.inputOptions.canopies.slice();
}),
),
tailles: this.jumpForm.controls['taille'].valueChanges.pipe(
startWith(''),
map(value => {
const taille = typeof value === 'string' ? value : value?.taille.toString();
return taille ? this._filterTaille(taille as string) : this.inputOptions.canopies.slice();
}),
),
dropzones: this.jumpForm.controls['lieu'].valueChanges.pipe(
startWith(''),
map(value => {
const lieu = typeof value === 'string' ? value : value?.lieu;
return lieu ? this._filterOaci(lieu as string) : this.inputOptions.dropzones.slice();
}),
),
oaci: this.jumpForm.controls['oaci'].valueChanges.pipe(
startWith(''),
map(value => {
const oaci = typeof value === 'string' ? value : value?.oaci;
return oaci ? this._filterOaci(oaci as string) : this.inputOptions.dropzones.slice();
}),
),
categories: this.jumpForm.controls['categorie'].valueChanges.pipe(
startWith(''),
map(value => {
const category = typeof value === 'string' ? value : value?.categorie;
return category ? this._filterModule(category as string) : this.inputOptions.categories.slice();
}),
),
modules: this.jumpForm.controls['module'].valueChanges.pipe(
startWith(''),
map(value => {
const module = typeof value === 'string' ? value : value?.module;
return module ? this._filterModule(module as string) : this.inputOptions.categories.slice();
}),
)
};
this._loadAeronefByImat();
this._loadCanopyModelBySize();
this._loadDropZoneByOaci();
this._loadJumpByModule();
}
ngOnDestroy() {
this._aeronefByImat.unsubscribe();
this._canopyModelBySize.unsubscribe();
this._dropZoneByOaci.unsubscribe();
this._jumpByModule.unsubscribe();
}
private _filterImat(imat: string): Array<AeronefByImat> {
const filterValue = imat.toLowerCase();
return this.inputOptions.aeronefs.filter(option => (option.aeronef.toLowerCase().includes(filterValue) || option.imat.toLowerCase().includes(filterValue)));
}
private _filterModule(module: string): Array<JumpByModule> {
const filterValue = module.toLowerCase();
return this.inputOptions.categories.filter(option => (option.categorie.toLowerCase().includes(filterValue) || option.module.toLowerCase().includes(filterValue)));
}
private _filterOaci(oaci: string): Array<DropZoneByOaci> {
const filterValue = oaci.toLowerCase();
return this.inputOptions.dropzones.filter(option => (option.lieu.toLowerCase().includes(filterValue) || option.oaci.toLowerCase().includes(filterValue)));
}
private _filterTaille(taille: string): Array<CanopyModelBySize> {
const filterValue = taille.toLowerCase();
return this.inputOptions.canopies.filter(option => (option.voile.toLowerCase().includes(filterValue) || option.taille.toString().includes(filterValue)));
}
private _loadAeronefByImat(): void {
const aeronefs$ = this._aeronefsService.getAllByImat();
this._aeronefByImat = aeronefs$.subscribe((aggregate: Array<AeronefByImat>) => {
this.inputOptions.aeronefs = aggregate;
});
}
private _loadCanopyModelBySize(): void {
const canopies$ = this._canopiesService.getAllBySizeByModel();
this._canopyModelBySize = canopies$.subscribe((aggregate: Array<CanopyModelBySize>) => {
this.inputOptions.canopies = aggregate;
});
}
private _loadDropZoneByOaci(): void {
const dropzones$ = this._dropZonesService.getAllByOaci();
this._dropZoneByOaci = dropzones$.subscribe((aggregate: Array<DropZoneByOaci>) => {
this.inputOptions.dropzones = aggregate;
});
}
private _loadJumpByModule(): void {
const jumps$ = this._jumpsService.getAllByModule();
this._jumpByModule = jumps$.subscribe((aggregate: Array<JumpByModule>) => {
this.inputOptions.categories = aggregate;
});
}
addSautant(): void {
const tag = this.tagField.value;
if (tag != null && tag.trim() !== '' && this.jump.sautants.indexOf(tag) < 0) {
this.jump.sautants.push(tag);
}
const participants = (this.jump.sautants.length + 1);
this.jumpForm.controls['participants'].setValue(participants);
this.tagField.reset('');
}
closeDialog(): void {
// close the dialog without result
this.dialogRef.close();
}
displayAeronefFn(aeronef: AeronefByImat): string {
let res = '';
if (aeronef) {
if (typeof aeronef === 'string') {
res = aeronef;
} else {
res = aeronef.aeronef ? aeronef.aeronef : '';
}
}
return res;
}
displayImatFn(aeronef: AeronefByImat): string {
let res = '';
if (aeronef) {
if (typeof aeronef === 'string') {
res = aeronef;
} else {
res = aeronef.imat ? aeronef.imat : '';
}
}
return res;
}
displayLieuFn(dropzone: DropZoneByOaci): string {
let res = '';
if (dropzone) {
if (typeof dropzone === 'string') {
res = dropzone;
} else {
res = dropzone.lieu ? dropzone.lieu : '';
}
}
return res;
}
displayOaciFn(dropzone: DropZoneByOaci): string {
let res = '';
if (dropzone) {
if (typeof dropzone === 'string') {
res = dropzone;
} else {
res = dropzone.oaci ? dropzone.oaci : '';
}
}
return res;
}
displayTailleFn(canopy: CanopyModelBySize): string {
let res = '';
if (canopy) {
if (typeof canopy === 'string') {
res = canopy;
} else {
res = canopy.taille ? canopy.taille.toString() : '';
}
}
return res;
}
displayVoileFn(canopy: CanopyModelBySize): string {
let res = '';
if (canopy) {
if (typeof canopy === 'string') {
res = canopy;
} else {
res = canopy.voile ? canopy.voile : '';
}
}
return res;
}
displayCategorieFn(jump: JumpByModule): string {
let res = '';
if (jump) {
if (typeof jump === 'string') {
res = jump;
} else {
res = jump.categorie ? jump.categorie : '';
}
}
return res;
}
displayModuleFn(jump: JumpByModule): string {
let res = '';
if (jump) {
if (typeof jump === 'string') {
res = jump;
} else {
res = jump.module ? jump.module : '';
}
}
return res;
}
getErrorMessage(name: string): string {
if (this.jumpForm.controls[name].errors !== null) {
return `Ce champ est requis.`;
}
return '';
}
onSelAeronef(aeronef: AeronefByImat) {
if (aeronef && aeronef.aeronef) {
this.jumpForm.controls['imat'].setValue(aeronef);
}
}
onSelImat(aeronef: AeronefByImat) {
if (aeronef && aeronef.imat) {
this.jumpForm.controls['aeronef'].setValue(aeronef);
}
}
onSelLieu(dropzone: DropZoneByOaci) {
if (dropzone && dropzone.oaci) {
this.jumpForm.controls['oaci'].setValue(dropzone);
}
}
onSelOaci(dropzone: DropZoneByOaci) {
if (dropzone && dropzone.lieu) {
this.jumpForm.controls['lieu'].setValue(dropzone);
}
}
onSelTaille(canopy: CanopyModelBySize) {
if (canopy && canopy.voile) {
this.jumpForm.controls['voile'].setValue(canopy);
}
}
onSelVoile(canopy: CanopyModelBySize) {
if (canopy && canopy.taille) {
this.jumpForm.controls['taille'].setValue(canopy);
}
}
onSelCategorie(jump: JumpByModule) {
if (jump && jump.categorie) {
this.jumpForm.controls['module'].setValue(jump);
}
}
onSelModule(jump: JumpByModule) {
if (jump && jump.module) {
this.jumpForm.controls['categorie'].setValue(jump);
}
}
removeSautant(index: string): void {
this.jump.sautants = this.jump.sautants.filter((sautant) => sautant !== index);
const participants = (this.jump.sautants.length + 1);
this.jumpForm.controls['participants'].setValue(participants);
}
submitForm(): void {
// update the model
this.updateJump(this.jumpForm.value);
this.jump.lieu = typeof this.jumpForm.controls['lieu'].value === 'string' ? this.jumpForm.controls['lieu'].value : this.jumpForm.controls['lieu'].value.lieu;
this.jump.oaci = typeof this.jumpForm.controls['oaci'].value === 'string' ? this.jumpForm.controls['oaci'].value : this.jumpForm.controls['oaci'].value.oaci;
this.jump.aeronef = typeof this.jumpForm.controls['aeronef'].value === 'string' ? this.jumpForm.controls['aeronef'].value : this.jumpForm.controls['aeronef'].value.aeronef;
this.jump.imat = typeof this.jumpForm.controls['imat'].value === 'string' ? this.jumpForm.controls['imat'].value : this.jumpForm.controls['imat'].value.imat;
this.jump.voile = typeof this.jumpForm.controls['voile'].value === 'string' ? this.jumpForm.controls['voile'].value : this.jumpForm.controls['voile'].value.voile;
this.jump.taille = typeof this.jumpForm.controls['taille'].value === 'string' ? this.jumpForm.controls['taille'].value : this.jumpForm.controls['taille'].value.taille;
this.jump.categorie = typeof this.jumpForm.controls['categorie'].value === 'string' ? this.jumpForm.controls['categorie'].value : this.jumpForm.controls['categorie'].value.categorie;
this.jump.module = typeof this.jumpForm.controls['module'].value === 'string' ? this.jumpForm.controls['module'].value : this.jumpForm.controls['module'].value.module;
// close the dialog with result
this.dialogRef.close(this.jump);
}
updateJump(values: NonNullable<unknown>): void {
Object.assign(this.jump, values);
}
}
@@ -0,0 +1,214 @@
<h2 mat-dialog-title class="bg-navy-light text-white border-bottom border-light-subtle d-flex mb-3 pt-3 pb-2">
Saut n° {{jump.numero}}<span class="flex-spacer"></span>
<mat-icon fontIcon="close" class="pointer" (click)="closeDialog()"></mat-icon>
</h2>
<mat-dialog-content>
<div class="container">
<div class="d-flex border-bottom border-light-subtle pb-2 mb-3">
<div class="flex-fill">
<h5>Le {{ jump.date | date: 'dd/MM/yyyy' }} à {{ jump.lieu }} {{ jump.zone ? jump.zone : jump.oaci }} :</h5>
</div>
<span class="flex-spacer"></span>
<div class="align-self-center me-2">
<span class="text-accent fs-5">{{ jump.categorie }}</span>
@if (jump.module) {
<span class="fs-5 mx-2">/</span>
<span class="text-muted fs-5">{{ jump.module }}</span>
}
</div>
</div>
<div class="row">
<div class="col-lg-6 col-md-12">
<dl class="dl-horizontal">
<dt>Aeronef:</dt>
<dd>{{ jump.aeronef }} {{ jump.imat }}</dd>
<dt>Voile:</dt>
<dd>{{ jump.voile }} {{ jump.taille }} ft<sup>2</sup></dd>
@if (jump.programme) {
<dt>Programme:</dt>
<dd>{{ jump.programme }}</dd>
}
</dl>
</div>
<div class="col-lg-6 col-md-12">
<dl class="dl-horizontal">
@if (!jump.x2data) {
<dt>Hauteur:</dt>
<dd>{{ jump.hauteur }} m</dd>
<dt>Déploiement:</dt>
<dd>{{ jump.deploiement }} m</dd>
}
@for (file of jump.files; track file) {
<dt>Fichier {{ file.type }}</dt>
<dd>{{ file.name }}</dd>
}
</dl>
</div>
</div>
<mat-divider></mat-divider>
@if (jump.x2data) {
<div class="row my-3">
<div class="col-sm-4 col-xs-12 text-center">
<h6>Hauteur</h6>
<span class="text-primary fw-medium fs-5">{{ jump.x2data.altitude.exit }} m</span>
</div>
<div class="col-sm-4 col-xs-12 text-center">
<h6 class="d-flex justify-content-center"><mat-icon fontIcon="timer" class="me-1 fs-5"></mat-icon> Durée de la chute</h6>
<span class="text-primary fw-medium fs-5">{{formatDuration(jump.x2data.duration.freeFall)}}</span>
</div>
<div class="col-sm-4 col-xs-12 text-center">
<h6>Déploiement</h6>
<span class="text-primary fw-medium fs-5">{{ jump.x2data.altitude.deployment }} m</span>
</div>
</div>
<div class="row my-4">
<div class="{{ jump.accessoires ? 'col-sm-6 ' : '' }}col-xs-12 text-center">
<h5 class="d-flex justify-content-center mb-4"><mat-icon fontIcon="people" class="me-1"></mat-icon> Participants ({{ jump.sautants.length + 1 }})</h5>
@for (sautant of jump.sautants; track sautant) {
<span class="badge rounded-pill bg-primary mb-2 me-1 pe-3 py-1 fw-normal text-dark">
<mat-icon class="align-middle me-1" fontIcon="person"></mat-icon>
{{ sautant }}
</span>
}
<span class="badge rounded-pill bg-accent mb-2 me-1 pe-3 py-1 fw-normal text-dark">
<mat-icon class="align-middle me-1" fontIcon="person"></mat-icon>
{{ jump.author.username }}
</span>
</div>
@if (jump.accessoires) {
<div class="col-sm-6 col-xs-12 text-center">
<h5 class="d-flex justify-content-center mb-4"><mat-icon fontIcon="auto_awesome" class="me-1"></mat-icon> Accessoires</h5>
<span class="badge rounded-pill bg-navy-light mb-2 me-1 pe-3 py-1 fw-normal">
<mat-icon class="align-middle me-1" fontIcon="auto_awesome"></mat-icon>
{{ jump.accessoires }}
</span>
</div>
}
</div>
<div class="row my-3">
<div class="col-xs-12 text-center">
<h5 class="d-flex justify-content-center mb-3"><mat-icon fontIcon="speed" class="me-1"></mat-icon> Vitesse en chute</h5>
</div>
<div class="col-sm-3 col-xs-12 text-center">
<h6>Max Verticale</h6>
<span class="text-primary fw-medium fs-5">{{ (jump.x2data.speed.freeFall.vertical.max / 1000 * 60 * 60) | number : '1.0-0' }} km/h</span>
</div>
<div class="col-sm-3 col-xs-12 text-center">
<h6>Max Horizontale</h6>
<span class="text-primary fw-medium fs-5">{{ (jump.x2data.speed.freeFall.horizontal.max / 1000 * 60 * 60) | number : '1.0-0' }} km/h</span>
</div>
<div class="col-sm-3 col-xs-12 text-center">
<h6>Moyenne Verticale</h6>
<span class="text-primary fw-medium fs-5">{{ (jump.x2data.speed.freeFall.vertical.avg / 1000 * 60 * 60) | number : '1.0-0' }} km/h</span>
</div>
<div class="col-sm-3 col-xs-12 text-center">
<h6>Moyenne Horizontale</h6>
<span class="text-primary fw-medium fs-5">{{ (jump.x2data.speed.freeFall.horizontal.avg / 1000 * 60 * 60) | number : '1.0-0' }} km/h</span>
</div>
</div>
<mat-divider></mat-divider>
<div class="row mt-4 mb-3">
<div class="col-xs-12 text-center">
<h5 class="d-flex justify-content-center mb-3"><mat-icon fontIcon="speed" class="me-1"></mat-icon> Vitesse sous voile</h5>
</div>
<div class="col-sm-3 col-xs-12 text-center">
<h6>Max Verticale</h6>
<span class="text-primary fw-medium fs-5">{{ (jump.x2data.speed.underCanopy.vertical.max / 1000 * 60 * 60) | number : '1.0-0' }} km/h</span>
</div>
<div class="col-sm-3 col-xs-12 text-center">
<h6>Max Horizontale</h6>
<span class="text-primary fw-medium fs-5">{{ (jump.x2data.speed.underCanopy.horizontal.max / 1000 * 60 * 60) | number : '1.0-0' }} km/h</span>
</div>
<div class="col-sm-3 col-xs-12 text-center">
<h6>Moyenne Verticale</h6>
<span class="text-primary fw-medium fs-5">{{ (jump.x2data.speed.underCanopy.vertical.avg / 1000 * 60 * 60) | number : '1.0-0' }} km/h</span>
</div>
<div class="col-sm-3 col-xs-12 text-center">
<h6>Moyenne Horizontale</h6>
<span class="text-primary fw-medium fs-5">{{ (jump.x2data.speed.underCanopy.horizontal.avg / 1000 * 60 * 60) | number : '1.0-0' }} km/h</span>
</div>
</div>
<mat-divider></mat-divider>
<div class="row mt-3">
<div class="col-xs-12 text-center">
<h5 class="d-flex justify-content-center mb-3"><mat-icon fontIcon="square_foot" class="me-1"></mat-icon> Finesse</h5>
</div>
<div class="col-sm-6 col-xs-12 text-center">
<h6>Finesse en chute</h6>
</div>
<div class="col-sm-6 col-xs-12 text-center">
<h6>Finesse sous voile</h6>
</div>
</div>
<div class="row mb-3">
<div class="col-sm-3 col-xs-12 text-center">
<h6>Finesse Max</h6>
<span class="text-primary fw-medium fs-5">{{ jump.x2data.glideRatio.freeFall.max | number : '1.2-2' }}</span>
</div>
<div class="col-sm-3 col-xs-12 text-center">
<h6>Finesse Moyenne</h6>
<span class="text-primary fw-medium fs-5">{{ jump.x2data.glideRatio.freeFall.avg | number : '1.2-2' }}</span>
</div>
<div class="col-sm-3 col-xs-12 text-center">
<h6>Finesse Max</h6>
<span class="text-primary fw-medium fs-5">{{ jump.x2data.glideRatio.underCanopy.max | number : '1.2-2' }}</span>
</div>
<div class="col-sm-3 col-xs-12 text-center">
<h6>Finesse Moyenne</h6>
<span class="text-primary fw-medium fs-5">{{ jump.x2data.glideRatio.underCanopy.avg | number : '1.2-2' }}</span>
</div>
</div>
<mat-divider></mat-divider>
<div class="row my-3">
<div class="col-xs-12 text-center">
<h5 class="d-flex justify-content-center mb-3"><mat-icon fontIcon="straighten" class="me-1"></mat-icon> Distance</h5>
</div>
<div class="col-sm-3 col-xs-12 text-center">
<h6>Distance au largage</h6>
<span class="text-primary fw-medium fs-5">{{ jump.x2data.distance.exitFromLandingZone| number : '1.0-0' }} m</span>
</div>
<div class="col-sm-3 col-xs-12 text-center">
<h6 class="d-flex justify-content-center"><mat-icon fontIcon="timer" class="me-1 fs-5"></mat-icon> Durée sous voile</h6>
<span class="text-primary fw-medium fs-5">{{formatDuration(jump.x2data.duration.underCanopy)}}</span>
</div>
<div class="col-sm-3 col-xs-12 text-center">
<h6>Distance totale</h6>
<span class="text-primary fw-medium fs-5">{{ jump.x2data.distance.totalFlying | number : '1.0-0' }} m</span>
</div>
<div class="col-sm-3 col-xs-12 text-center">
<h6>Distance horizontale</h6>
<span class="text-primary fw-medium fs-5">{{ jump.x2data.distance.totalHorizontal | number : '1.0-0' }} m</span>
</div>
</div>
<mat-divider></mat-divider>
}
@else {
<div class="row my-4">
<div class="col-xs-12 text-center">
<h5 class="d-flex justify-content-center mb-4"><mat-icon fontIcon="people" class="me-1"></mat-icon> Participants ({{ jump.sautants.length + 1 }})</h5>
@for (sautant of jump.sautants; track sautant) {
<span class="badge rounded-pill bg-primary mb-2 me-1 pe-3 py-1 fw-normal text-dark">
<mat-icon class="align-middle me-1" fontIcon="person"></mat-icon>
{{ sautant }}
</span>
}
<span class="badge rounded-pill bg-accent mb-2 me-1 pe-3 py-1 fw-normal text-dark">
<mat-icon class="align-middle me-1" fontIcon="person"></mat-icon>
{{ jump.author.username }}
</span>
</div>
</div>
<mat-divider></mat-divider>
<div class="d-flex justify-content-center my-3 fs-5"><mat-icon fontIcon="satellite_alt" class="me-2"></mat-icon> Aucune donnée GPS</div>
<mat-divider></mat-divider>
}
</div>
</mat-dialog-content>
<mat-dialog-actions>
<span class="flex-spacer"></span>
<button mat-button (click)="closeDialog()" class="me-1">Fermer</button>
</mat-dialog-actions>
@@ -0,0 +1,37 @@
import { Component, Inject } from '@angular/core';
import { DatePipe, DecimalPipe } from '@angular/common';
import { MatButtonModule } from '@angular/material/button';
import { MatDialogRef, MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';
import { MatDividerModule } from '@angular/material/divider';
import { MatIconModule } from '@angular/material/icon';
import { Jump } from '@models';
@Component({
selector: 'app-jump-view-dialog',
templateUrl: 'jump-view.dialog.html',
standalone: true,
imports: [
DatePipe, DecimalPipe,
MatButtonModule, MatDialogModule,
MatDividerModule, MatIconModule
]
})
export class JumpViewDialogComponent {
constructor(
public dialogRef: MatDialogRef<JumpViewDialogComponent>,
@Inject(MAT_DIALOG_DATA) public jump: Jump
) { }
closeDialog(): void {
// close the dialog without result
this.dialogRef.close();
}
formatDuration(duration: number): string {
const start = new Date(0);
start.setSeconds(duration);
return start.toISOString().substring(14, 19);
}
}
@@ -0,0 +1,105 @@
<div @flyInOut>
<div class="d-flex">
<div class="flex-fill">
<h1 class="mb-1">{{title}}</h1>
<span [hidden]="!lastJump.numero" class="me-2 fs-6">
Dernier saut enregistré : <span class="fs-5 mx-1 text-primary">{{ lastJump.numero }}<sup>ème</sup></span> à <span class="fs-5 mx-1 text-primary">{{ lastJump.lieu }}</span> le <span class="fs-5 mx-1 text-primary">{{ lastJump.date | date: 'dd/MM/yyyy' }}</span>
</span>
</div>
<span class="flex-spacer"></span>
<div class="align-self-center me-2">
<button mat-icon-button (click)="openAddDialog()" aria-label="Ajouter">
<mat-icon fontIcon="add_notes"></mat-icon>
</button>
<button mat-icon-button [matMenuTriggerFor]="menuJump" aria-label="Menu Jump">
<mat-icon fontIcon="more_vert"></mat-icon>
</button>
<mat-menu #menuJump="matMenu">
<button mat-menu-item (click)="openAddDialog()">
<mat-icon fontIcon="add_notes"></mat-icon>
<span>Ajouter un saut</span>
</button>
@for (menuitem of menuItems.getMenuLogbook(); track menuitem) {
@if (menuitem.type === 'link') {
<button mat-menu-item [routerLink]="['/', menuitem.state]">
<mat-icon [fontIcon]="menuitem.icon"></mat-icon>
<span>{{ menuitem.name }}</span>
</button>
}
}
</mat-menu>
</div>
</div>
<mat-divider class="my-3"></mat-divider>
@if (jumpsToHundred() <= 20) {
<div class="d-flex pb-2 pe-2">
<span class="flex-spacer"></span>
<mat-icon fontIcon="warning" color="teal"></mat-icon>
<div class="text-end text-teal align-self-center ms-2">
{{ lastJump.numero + jumpsToHundred() }}<sup>ème</sup> à venir dans {{ jumpsToHundred() }}
{{ jumpsToHundred() > 1 ? 'sauts' : 'saut' }}
</div>
</div>
}
<mat-card>
<mat-card-header class="p-0">
<mat-expansion-panel class="w-100">
<mat-expansion-panel-header class="bg-accent">
<mat-panel-title>Filtrer les sauts</mat-panel-title>
@if (jumpsCount < lastJump.numero) {
<mat-panel-description>
{{ jumpsCount }} {{ jumpsCount > 1 ? 'sauts' : 'saut' }} sur {{ lastJump.numero }} correspondent à la recherche
</mat-panel-description>
}
</mat-expansion-panel-header>
<div class="d-flex mt-3 mb-0">
<div class="flex-fill">
<div class="label-container d-flex justify-content-between mx-2">
<label for="yearRange">Année du saut</label>
<label for="yearRange">de <span class="fw-bold">{{listConfig.filters.yearRangeStart}}</span> à <span class="fw-bold">{{listConfig.filters.yearRangeEnd}}</span></label>
</div>
<mat-slider [min]="rangesMinMax.year.start" [max]="rangesMinMax.year.end" step="1" discrete>
<input id="yearRange" matSliderStartThumb [(ngModel)]="listConfig.filters.yearRangeStart">
<input matSliderEndThumb [(ngModel)]="listConfig.filters.yearRangeEnd">
</mat-slider>
</div>
<div class="flex-fill">
<div class="label-container d-flex justify-content-between mx-2">
<label for="numeroRange">Numéro du saut</label>
<label for="numeroRange">de <span class="fw-bold">{{listConfig.filters.numeroRangeStart}}</span> à <span class="fw-bold">{{listConfig.filters.numeroRangeEnd}}</span></label>
</div>
<mat-slider [min]="rangesMinMax.numero.start" [max]="rangesMinMax.numero.end" step="1" discrete>
<input id="numeroRange" matSliderStartThumb [(ngModel)]="listConfig.filters.numeroRangeStart">
<input matSliderEndThumb [(ngModel)]="listConfig.filters.numeroRangeEnd">
</mat-slider>
</div>
<div class="flex-fill">
<div class="label-container d-flex justify-content-between mx-2">
<label for="tailleRange">Taille de la voile</label>
<label for="tailleRange">de <span class="fw-bold">{{listConfig.filters.tailleRangeStart}}</span> ft<sup>2</sup> à <span class="fw-bold">{{listConfig.filters.tailleRangeEnd}}</span> ft<sup>2</sup></label>
</div>
<mat-slider [min]="rangesMinMax.taille.start" [max]="rangesMinMax.taille.end" step="1" discrete>
<input id="tailleRange" matSliderStartThumb [(ngModel)]="listConfig.filters.tailleRangeStart">
<input matSliderEndThumb [(ngModel)]="listConfig.filters.tailleRangeEnd">
</mat-slider>
</div>
<div class="flex-fill">
<div class="label-container d-flex justify-content-between mx-2">
<label for="participantsRange">Taille du groupe</label>
<label for="participantsRange">de <span class="fw-bold">{{listConfig.filters.participantsRangeStart}}</span> à <span class="fw-bold">{{listConfig.filters.participantsRangeEnd}}</span></label>
</div>
<mat-slider [min]="rangesMinMax.participants.start" [max]="rangesMinMax.participants.end" step="1" discrete>
<input id="participantsRange" matSliderStartThumb [(ngModel)]="listConfig.filters.participantsRangeStart">
<input matSliderEndThumb [(ngModel)]="listConfig.filters.participantsRangeEnd">
</mat-slider>
</div>
<span class="flex-spacer"></span>
</div>
</mat-expansion-panel>
</mat-card-header>
<mat-card-content class="mt-2">
<app-jump-table (countChange)="setTotalResults($event)" #jumpTable [config]="listConfig" [isUser]="isUser" [showAdd]="canModify" [title]="title">
</app-jump-table>
</mat-card-content>
</mat-card>
</div>
@@ -0,0 +1,11 @@
.mat-expansion-panel {
border-bottom-right-radius: initial;
border-bottom-left-radius: initial;
}
.mat-mdc-slider {
max-width: 300px;
width: 100%;
}
.label-container {
max-width: 284px;
}
@@ -0,0 +1,21 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { LogbookComponent } from './logbook.component';
describe('LogbookComponent', () => {
let component: LogbookComponent;
let fixture: ComponentFixture<LogbookComponent>;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [LogbookComponent]
});
fixture = TestBed.createComponent(LogbookComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,399 @@
import { CommonModule } from '@angular/common';
import { trigger, state, style, animate, transition } from '@angular/animations';
import { Component, AfterContentChecked, OnInit, OnDestroy, computed, signal, Signal } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { ActivatedRoute, Router, RouterModule } from '@angular/router';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatDividerModule } from '@angular/material/divider';
import { MatDialog, MatDialogModule } from '@angular/material/dialog';
import { MatExpansionModule } from '@angular/material/expansion';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { MatMenuModule } from '@angular/material/menu';
import { MatSliderModule } from '@angular/material/slider';
import { MatSnackBar, MatSnackBarConfig, MatSnackBarHorizontalPosition, MatSnackBarModule, MatSnackBarVerticalPosition } from '@angular/material/snack-bar';
import { Observable, Subscription } from 'rxjs';
import { take } from 'rxjs/operators';
import { JumpAddDialogComponent } from '@components/logbook/dialogs';
import { MenuItems, JumpTableComponent } from '@components/shared';
import { Errors, Jump, JumpFile, JumpFileType, JumpListConfig, Range, User, X2Data } from '@models';
import { JumpsService, JumpTableService, UserService } from '@services';
import data from 'src/jumps.json';
//import data from 'src/jumps_02.json';
@Component({
standalone: true,
imports: [
CommonModule, RouterModule, FormsModule,
MatButtonModule, MatCardModule, MatDividerModule, MatExpansionModule,
MatIconModule, MatInputModule, MatMenuModule, MatDialogModule,
MatSliderModule, MatSnackBarModule,
JumpTableComponent
],
selector: 'app-logbook',
templateUrl: './logbook.component.html',
styleUrl: './logbook.component.scss',
animations: [
trigger('flyInOut', [
state('in', style({ transform: 'translateY(0)' })),
transition('void => *', [
style({ transform: 'translateY(-100%)' }),
animate(200)
]),
transition('* => void', [
style({ transform: 'translateY(100%)' }),
animate(200)
])
])
]
})
export class LogbookComponent implements OnInit, OnDestroy, AfterContentChecked {
private _currentUser: Subscription = new Subscription();
private _subscriptions: Array<Subscription> = [];
private _data: Subscription = new Subscription();
private _lastjump: Subscription = new Subscription();
private _lastjump$: Observable<Jump> = new Observable();
public jumpsCount = 0;
public errors!: Errors;
public title = 'Carnet de sauts';
public jump: Jump = {} as Jump;
public lastJump: Jump = {} as Jump;
public currentUser: User = {} as User;
public listConfig: JumpListConfig = { type: 'all', filters: {} };
public isAuthenticated = false;
public isUser = true;
public canModify = false;
public isSubmitting = false;
public tableRefresh = false;
public jumpRefresh = false;
public jumpsSinceHundred: Signal<number> = signal(0);
public jumpsToHundred: Signal<number> = signal(100);
public rangesMinMax: {
numero: Range;
taille: Range;
participants: Range;
hauteur: Range;
year: Range;
} = {
numero: {start: 1, end: 100000},
taille: {start: 1, end: 400},
participants: {start: 1, end: 200},
hauteur: {start: 0, end: 10000},
year: {start: 1970, end: 2020}
};
constructor(
private route: ActivatedRoute,
private router: Router,
private dialog: MatDialog,
private snackBar: MatSnackBar,
private _jumpsService: JumpsService,
private _jumpTableService: JumpTableService,
private _userService: UserService,
public menuItems: MenuItems
) { }
ngOnInit() {
//this.importJumps();
const data$: Observable<{isAuthenticated: boolean, lastjump: Jump}> = this.route.data as Observable<{isAuthenticated: boolean, lastjump: Jump}>;
this._data = data$.subscribe((data: {isAuthenticated: boolean, lastjump: Jump}) => {
this.isAuthenticated = data.isAuthenticated;
if (!this.isAuthenticated) {
this.router.navigateByUrl('/login');
return;
}
const currentUser$: Observable<User> = this._userService.currentUser;
this._currentUser = currentUser$.subscribe((userData: User) => {
this.currentUser = userData;
this.canModify = this.currentUser.role === 'Admin';
});
const currentYear: number = new Date().getFullYear();
this.lastJump = data['lastjump'];
this.rangesMinMax = {
numero: {start: 1, end: data.lastjump.numero},
taille: {start: data.lastjump.taille!, end: 230},
participants: {start: 1, end: 20},
hauteur: {start: 1000, end: 8000},
year: {start: 2018, end: currentYear}
}
this.listConfig.filters.numeroRangeStart = this.rangesMinMax.numero.start;
this.listConfig.filters.numeroRangeEnd = this.rangesMinMax.numero.end;
this.listConfig.filters.tailleRangeStart = this.rangesMinMax.taille.start;
this.listConfig.filters.tailleRangeEnd = this.rangesMinMax.taille.end;
this.listConfig.filters.participantsRangeStart = this.rangesMinMax.participants.start;
this.listConfig.filters.participantsRangeEnd = (this.rangesMinMax.participants.end / 2);
this.listConfig.filters.hauteurRangeStart = this.rangesMinMax.hauteur.start;
this.listConfig.filters.hauteurRangeEnd = this.rangesMinMax.hauteur.end;
this.listConfig.filters.yearRangeStart = this.rangesMinMax.year.start;
this.listConfig.filters.yearRangeEnd = this.rangesMinMax.year.end;
this.jumpsSinceHundred = computed(() => (this.lastJump.numero % 100));
this.jumpsToHundred = computed(() => (100 - this.jumpsSinceHundred()));
/*
this._lastjump$ = this._jumpsService.getLastJump();
this._lastjump = this._lastjump$.subscribe((jump) => {
const currentYear: number = new Date().getFullYear();
this.lastJump = jump;
this.rangesMinMax = {
numero: {start: 1, end: jump.numero},
taille: {start: jump.taille!, end: 230},
participants: {start: 1, end: 20},
hauteur: {start: 1000, end: 8000},
year: {start: 2018, end: currentYear}
}
this.listConfig.filters.numeroRangeStart = this.rangesMinMax.numero.start;
this.listConfig.filters.numeroRangeEnd = this.rangesMinMax.numero.end;
this.listConfig.filters.tailleRangeStart = this.rangesMinMax.taille.start;
this.listConfig.filters.tailleRangeEnd = this.rangesMinMax.taille.end;
this.listConfig.filters.participantsRangeStart = this.rangesMinMax.participants.start;
this.listConfig.filters.participantsRangeEnd = (this.rangesMinMax.participants.end / 2);
this.listConfig.filters.hauteurRangeStart = this.rangesMinMax.hauteur.start;
this.listConfig.filters.hauteurRangeEnd = this.rangesMinMax.hauteur.end;
this.listConfig.filters.yearRangeStart = this.rangesMinMax.year.start;
this.listConfig.filters.yearRangeEnd = this.rangesMinMax.year.end;
this.jumpsSinceHundred = computed(() => (this.lastJump.numero % 100));
this.jumpsToHundred = computed(() => (100 - this.jumpsSinceHundred()));
});
*/
});
}
ngOnDestroy() {
this._data.unsubscribe();
this._currentUser.unsubscribe();
this._lastjump.unsubscribe();
this._subscriptions.forEach(subscription => {
subscription.unsubscribe();
});
}
ngAfterContentChecked() {
if (this.tableRefresh != this._jumpTableService.tableRefresh) {
this.tableRefresh = this._jumpTableService.tableRefresh;
this._lastjump$ = this._jumpsService.getLastJump();
this._lastjump = this._lastjump$.subscribe((jump) => {
this.lastJump = jump;
});
console.log('lastjump has been updated in logbook component');
}
}
importJumps() {
try {
data.forEach((entry) => {
Object.assign(this.jump, entry);
this._jumpsService.save(this.jump)
.pipe(take(1))
.subscribe({
next: (jump) => {
console.log(`Le saut n°${jump.numero} a été ajouté !`, 'OK');
},
error: (err) => {
console.log(`Le saut n'a pas été ajouté !`, 'Error', err);
this.errors = err;
}
});
});
} catch (error) {
console.error(error);
}
}
openAddDialog(): void {
this.jump = <Jump>{};
this.jump.numero = (this.lastJump.numero + 1);
this.jump.sautants = [];
this.jump.participants = 1;
const dialogRef = this.dialog.open(JumpAddDialogComponent, {
width: '70vw',
data: {jump: this.jump, lastJump: this.lastJump}
});
dialogRef.afterClosed().pipe(take(1))
.subscribe(result => {
if (result != undefined) {
this._onEntry(result);
}
});
}
setTotalResults(value: number): void {
this.jumpsCount = value;
}
private _loadSkydiverIdJumps(jumps: Array<Jump>) {
const config: JumpListConfig = { type: 'all', filters: {} } as JumpListConfig;
config.filters.limit = 200;
config.filters.offset = 0;
config.filters.dateRangeStart = `${jumps[0].date!.substring(0, 10)} 00:00:00`;
config.filters.dateRangeEnd = `${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 = jumps.length - data.items.length;
if (diff < 0) {
console.error(`${diff} ${diff > 1 ? 'données' : 'donnée'} altimètre de trop le ${jumps[0].date!.substring(0, 10)}`);
}
if (diff > 0) {
console.error(`${diff}/${jumps.length} ${diff > 1 ? 'données altimètre manquantes' : 'donnée altimètre manquante'} le ${jumps[0].date!.substring(0, 10)}`, jumps, data.items);
}
console.log(`${jumps.length} ${jumps.length > 1 ? 'sauts' : 'saut'} le ${data.items[0].date!.substring(0, 10)}`, jumps.sort((a, b) => b.numero! - a.numero!), data.items);
this._addX2Data(data.items, jumps);
} else {
console.info('Aucune données altimètre pour le ', jumps[0].date!.substring(0, 10));
}
},
error: (err) => {
console.error('getAllFromSkydiverIdApi error');
this.errors = err;
}
});
this._subscriptions.push(subscription);
}
private _addJump(jumps: Array<Jump>, index: number = 0) {
this._jumpsService.save(jumps[index])
.pipe(take(1))
.subscribe({
next: (jump) => {
jumps[index] = jump;
console.log('Ajout du saut', jump.numero);
Object.assign(this.jump, jump);
this._openSnackBar(`Le saut n°${jump.numero} a été ajouté !`, 'Annuler');
this._resetErrors();
if (jumps.length == (index + 1)) {
this._loadSkydiverIdJumps(jumps);
} else {
this._addJump(jumps, (index + 1));
}
},
error: (err) => {
this.errors = err;
this.isSubmitting = false;
}
});
}
private _addX2Data(x2data: Array<X2Data>, jumps: Array<Jump>, index: number = 0) {
const file: JumpFile = {
name: x2data[index].name,
path: `/Volumes/Storage/Skydive/X2_Logs/${x2data[index].date!.substring(0, 4)}/`,
type: JumpFileType.CSV
} as JumpFile;
this._subscriptions.push(
this._jumpsService.saveX2Data(jumps[index].slug!, file, x2data[index])
.pipe(take(1))
.subscribe({
next: (jump) => {
jumps[index] = jump;
console.log(`Le fichier ${file.name} a été ajouté au saut n°${jump.numero} !`, 'OK');
if (jumps.length == (index + 1) || x2data.length == (index + 1)) {
this._addKmlFile(jumps);
} else {
this._addX2Data(x2data, jumps, (index + 1));
}
},
error: (err) => {
this.errors = err;
}
})
);
}
private _addKmlFile(jumps: Array<Jump>, index: number = 0) {
if (jumps[index].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: jumps[index].x2data.name.substring(0, -4) + '.kml',
path: `/Volumes/Storage/Skydive/X2_Kmls/${jumps[index].date.substring(0, 4)}/`,
type: JumpFileType.KML
} as JumpFile;
this._subscriptions.push(
this._jumpsService.saveKml(jumps[index].slug, file)
.pipe(take(1))
.subscribe({
next: (file) => {
console.log(`Le fichier ${file.path}${file.name} a été ajouté au saut n°${jumps[index].numero} !`, 'OK');
if (jumps.length == (index + 1)) {
this._resetErrors();
this.jumpRefresh = !this.jumpRefresh;
this._jumpTableService.updateJumpRefresh(this.jumpRefresh);
this._lastjump$ = this._jumpsService.getLastJump();
this._lastjump = this._lastjump$.subscribe((jump) => {
this.lastJump = jump;
});
} else {
this._addKmlFile(jumps, (index + 1));
}
},
error: (err) => {
this.errors = err;
}
})
);
}
}
private _onEntry(jumps: Array<Jump>): void {
if (jumps.length) {
this._addJump(jumps);
}
}
/*
private _onEntry(jumps: Array<Jump>): void {
const max: number = jumps.length;
let refresh: boolean = false;
jumps.forEach((jump: Jump, index: number) => {
console.log('Ajout du saut', jump.numero);
Object.assign(this.jump, jump);
if (max == index+1) {
refresh = true;
}
this._saveJump(jump, refresh);
});
}
private _saveJump(jump: Jump, refresh: boolean) {
this._jumpsService.save(jump)
.pipe(take(1))
.subscribe({
next: (jump) => {
this._openSnackBar(`Le saut n°${jump.numero} a été ajouté !`, 'Annuler');
this._resetErrors();
if (refresh) {
this.jumpRefresh = !this.jumpRefresh;
this._jumpTableService.updateJumpRefresh(this.jumpRefresh);
this._lastjump$ = this._jumpsService.getLastJump();
this._lastjump = this._lastjump$.subscribe((jump) => {
this.lastJump = jump;
});
}
},
error: (err) => {
this.errors = err;
this.isSubmitting = false;
}
});
}
*/
private _openSnackBar(content: string, title: string) {
const config: MatSnackBarConfig = {
horizontalPosition: <MatSnackBarHorizontalPosition>'end',
verticalPosition: <MatSnackBarVerticalPosition>'bottom',
duration: 4000
};
this.snackBar.open(content, title, config);
}
private _resetErrors(): void {
this.errors = { errors: {} };
}
}
@@ -0,0 +1,30 @@
<div @flyInOut>
<div class="d-flex">
<div class="flex-fill">
<h1 class="mb-1">{{ article.title }}</h1>
</div>
<span class="flex-spacer"></span>
<div class="align-self-center me-2 text-end">
<span class="fs-5 text-primary">{{ article.createdAt | date: 'dd/MM/yyyy' }}</span>
</div>
</div>
<mat-divider class="my-3"></mat-divider>
<app-list-errors [errors]="errors"></app-list-errors>
<div class="d-flex">
{{ article.description }}
</div>
<div class="d-flex">
{{ article.body }}
</div>
@if (article.tagList) {
<mat-divider class="my-3"></mat-divider>
<div class="d-flex">
@for (tag of getArticleTags(); track tag) {
<span class="badge rounded-pill bg-accent mb-2 me-1 pe-3 py-1 fw-normal text-dark">
<mat-icon class="align-middle me-1" fontIcon="tag"></mat-icon>
{{ tag.name }}
</span>
}
</div>
}
</div>
@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { PageComponent } from './page.component';
describe('PageComponent', () => {
let component: PageComponent;
let fixture: ComponentFixture<PageComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [PageComponent]
})
.compileComponents();
fixture = TestBed.createComponent(PageComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
+69
View File
@@ -0,0 +1,69 @@
import { Title } from '@angular/platform-browser';
import { trigger, state, style, animate, transition } from '@angular/animations';
import { Component, DestroyRef, inject, OnInit, OnDestroy } from '@angular/core';
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { DatePipe } from '@angular/common';
import { ActivatedRoute, RouterLink } from '@angular/router';
import { MatCardModule } from '@angular/material/card';
import { MatDividerModule } from '@angular/material/divider';
import { MatIconModule } from '@angular/material/icon';
import { Observable, Subscription } from 'rxjs';
import { ListErrorsComponent, } from '@components/shared';
import { Errors, Article, ArticlePageData } from '@models';
@Component({
selector: 'app-page',
standalone: true,
imports: [
DatePipe, RouterLink,
MatCardModule, MatDividerModule, MatIconModule,
ListErrorsComponent
],
templateUrl: './page.component.html',
styleUrl: './page.component.scss',
animations: [
trigger('flyInOut', [
state('in', style({ transform: 'translateY(0)' })),
transition('void => *', [
style({ transform: 'translateY(-100%)' }),
animate(200)
]),
transition('* => void', [
style({ transform: 'translateY(100%)' }),
animate(200)
])
])
]
})
export class PageComponent implements OnInit, OnDestroy {
private _data: Subscription = new Subscription();
public title = 'Shop bientôt disponible!';
public description = 'Encore un peu de patience, notre shop sera mis en ligne d\'ici peu.';
public errors: Errors = { errors: {} };
public destroyRef = inject(DestroyRef);
public article: Article = {} as Article;
constructor(
private route: ActivatedRoute,
private titleService: Title
) { }
ngOnInit() {
const data$: Observable<{ pageData: ArticlePageData }> = this.route.data as Observable<{ pageData: ArticlePageData }>;
this._data = data$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((data: { pageData: ArticlePageData }) => {
this.article = data.pageData.article;
this.title = this.article.title;
this.titleService.setTitle(this.article.title);
console.log(this.article);
});
}
ngOnDestroy() {
this._data.unsubscribe();
}
getArticleTags() {
return this.article.tagList;
}
}
@@ -0,0 +1 @@
<p>pages works!</p>
@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { PagesComponent } from './pages.component';
describe('PagesComponent', () => {
let component: PagesComponent;
let fixture: ComponentFixture<PagesComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [PagesComponent]
})
.compileComponents();
fixture = TestBed.createComponent(PagesComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,12 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-pages',
standalone: true,
imports: [],
templateUrl: './pages.component.html',
styleUrl: './pages.component.scss'
})
export class PagesComponent {
}
@@ -0,0 +1,28 @@
<div @flyInOut>
<div class="d-flex">
<div class="flex-fill">
<h1 class="mb-1">{{ product.name }}<small>- Sachet {{ product.weightNet | number : '1.0' }} g</small></h1>
<h2 class="mb-1">{{ (product.priceRetail! * (1 + (product.priceTaxe! / 100))) | number : '1.2-2' }} € TTC</h2>
</div>
<span class="flex-spacer"></span>
<div class="align-self-center me-2 text-end">
<span class="fs-5 text-primary">{{ product.createdAt | date: 'dd/MM/yyyy' }}</span>
</div>
</div>
<mat-divider class="my-3"></mat-divider>
<app-list-errors [errors]="errors"></app-list-errors>
<div class="d-flex">
{{ product.description }}
</div>
@if (product.tags) {
<mat-divider class="my-3"></mat-divider>
<div class="d-flex">
@for (tag of getProductTags(); track tag) {
<span class="badge rounded-pill bg-accent mb-2 me-1 pe-3 py-1 fw-normal text-dark">
<mat-icon class="align-middle me-1" fontIcon="tag"></mat-icon>
{{ tag.name }}
</span>
}
</div>
}
</div>
@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ProductComponent } from './product.component';
describe('ProductComponent', () => {
let component: ProductComponent;
let fixture: ComponentFixture<ProductComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [ProductComponent]
})
.compileComponents();
fixture = TestBed.createComponent(ProductComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,70 @@
import { Title } from '@angular/platform-browser';
import { trigger, state, style, animate, transition } from '@angular/animations';
import { Component, DestroyRef, inject, OnInit, OnDestroy } from '@angular/core';
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { DatePipe, DecimalPipe } from '@angular/common';
import { ActivatedRoute, RouterLink } from '@angular/router';
import { MatCardModule } from '@angular/material/card';
import { MatDividerModule } from '@angular/material/divider';
import { MatIconModule } from '@angular/material/icon';
import { Observable, Subscription } from 'rxjs';
import { ListErrorsComponent, } from '@components/shared';
import { Errors, Product, ProductPageData } from '@models';
@Component({
selector: 'app-product',
standalone: true,
imports: [
DatePipe, DecimalPipe, RouterLink,
MatCardModule, MatDividerModule, MatIconModule,
ListErrorsComponent
],
templateUrl: './product.component.html',
styleUrl: './product.component.scss',
animations: [
trigger('flyInOut', [
state('in', style({ transform: 'translateY(0)' })),
transition('void => *', [
style({ transform: 'translateY(-100%)' }),
animate(200)
]),
transition('* => void', [
style({ transform: 'translateY(100%)' }),
animate(200)
])
])
]
})
export class ProductComponent implements OnInit, OnDestroy {
private _data: Subscription = new Subscription();
public title = 'Ad Astra - Produit';
public description = 'Encore un peu de patience, notre shop sera mis en ligne d\'ici peu.';
public errors: Errors = { errors: {} };
public destroyRef = inject(DestroyRef);
public product: Product = {} as Product;
constructor(
private route: ActivatedRoute,
private titleService: Title
) { }
ngOnInit() {
const data$: Observable<{ pageData: ProductPageData }> = this.route.data as Observable<{ pageData: ProductPageData }>;
this._data = data$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((data: { pageData: ProductPageData }) => {
this.product = data.pageData.product;
this.title = this.product.name;
this.description = this.product.description;
this.titleService.setTitle(this.product.name);
console.log(this.product);
});
}
ngOnDestroy() {
this._data.unsubscribe();
}
getProductTags() {
return this.product.tags;
}
}
@@ -0,0 +1,50 @@
<div @flyInOut>
<div class="d-flex">
<div class="flex-fill">
<h1 class="mb-1">{{ title }} <span class="ms-1 fs-3 text-muted">({{ productsCount }})</span></h1>
</div>
<span class="flex-spacer"></span>
<div class="align-self-center me-2 text-end">
</div>
</div>
<mat-divider class="my-3"></mat-divider>
<app-list-errors [errors]="errors"></app-list-errors>
<div class="d-flex">
<div class="fs-4" [innerHtml]="description"></div>
</div>
<mat-divider class="my-3"></mat-divider>
<div class="row">
@for (product of products; track product) {
<div class="col-xs-12 col-sm-6 col-md-4 col-xl-3">
<mat-card class="mb-3">
<mat-card-header class="d-flex rounded-top py-2 px-4">
<div class="flex-fill">
<mat-card-title>
{{ product.name }}
</mat-card-title>
</div>
<span class="flex-spacer"></span>
<div class="align-self-center me-2 text-end">
<span class="fs-4">Sachet {{ product.weightNet | number : '1.0' }} g</span>
</div>
</mat-card-header>
<mat-card-content>
<p class="card-text">
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris sodales ultrices dolor, ullamcorper cursus mauris.<br />
Nam risus metus, porttitor eget lacinia nec, mollis ac augue. In hac habitasse platea dictumst.<br />
Aliquam vel volutpat diam. Integer sed nulla posuere, commodo augue ac, tincidunt.
</p>
<mat-card-subtitle>
{{ product.productCategories[0].name }} {{ product.brand.name }}
</mat-card-subtitle>
<div class="align-self-center me-2 text-end">
<span class="fs-4 fw-medium text-primary">{{ (product.priceRetail! * (1 + (product.priceTaxe! / 100))) | number : '1.2-2' }} € TTC</span>
<button mat-flat-button class="ms-2" color="teal" type="button">Ajouter au panier</button>
</div>
</mat-card-content>
</mat-card>
</div>
}
</div>
</div>
@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ProductsComponent } from './products.component';
describe('ProductsComponent', () => {
let component: ProductsComponent;
let fixture: ComponentFixture<ProductsComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [ProductsComponent]
})
.compileComponents();
fixture = TestBed.createComponent(ProductsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,72 @@
import { Title } from '@angular/platform-browser';
import { trigger, state, style, animate, transition } from '@angular/animations';
import { Component, DestroyRef, inject, OnInit, OnDestroy } from '@angular/core';
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { DatePipe, DecimalPipe } from '@angular/common';
import { ActivatedRoute, RouterLink } from '@angular/router';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatDividerModule } from '@angular/material/divider';
import { MatIconModule } from '@angular/material/icon';
import { Observable, Subscription } from 'rxjs';
import { ListErrorsComponent, } from '@components/shared';
import { Errors, Product, ProductsPageData } from '@models';
@Component({
selector: 'app-products',
standalone: true,
imports: [
DatePipe, DecimalPipe, RouterLink,
MatButtonModule, MatCardModule, MatDividerModule, MatIconModule,
ListErrorsComponent
],
templateUrl: './products.component.html',
styleUrl: './products.component.scss',
animations: [
trigger('flyInOut', [
state('in', style({ transform: 'translateY(0)' })),
transition('void => *', [
style({ transform: 'translateY(-100%)' }),
animate(200)
]),
transition('* => void', [
style({ transform: 'translateY(100%)' }),
animate(200)
])
])
]
})
export class ProductsComponent implements OnInit, OnDestroy {
private _data: Subscription = new Subscription();
public title = 'Ad Astra - Produit';
public description = 'Encore un peu de patience, notre shop sera mis en ligne d\'ici peu.';
public errors: Errors = { errors: {} };
public destroyRef = inject(DestroyRef);
public products: Array<Product> = [];
public productsCount = 0;
constructor(
private route: ActivatedRoute,
private titleService: Title
) { }
ngOnInit() {
const data$: Observable<{ pageData: ProductsPageData }> = this.route.data as Observable<{ pageData: ProductsPageData }>;
this._data = data$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((data: { pageData: ProductsPageData }) => {
this.products = data.pageData.products;
this.productsCount = data.pageData.count;
//this.title = 'Fleurs CBD';
this.title = data.pageData.products[0].productCategories[0]!.name;
this.description = data.pageData.products[0].productCategories[0]!.description;
this.titleService.setTitle(this.title);
console.log(data.pageData);
console.log(this.products);
});
}
ngOnDestroy() {
this._data.unsubscribe();
}
}
@@ -0,0 +1,158 @@
<div class="banner mat-elevation-z2">
<div class="banner-content d-flex text-bg-accent bg-opacity-50">
<img [src]="user.image" class="user-img me-3" [alt]="user.username" />
<div class="flex-spacer">
<h3 class="mb-0">{{bannerTitle}}</h3>
<span class="fs-6">{{user.firstname}} {{user.lastname}}</span>
</div>
<div class="align-self-center text-end">
<button mat-raised-button color="warn" routerLink="/settings">
<mat-icon aria-label="Modifier le compte" fontIcon="settings"></mat-icon> {{ btnSettingsTitle }}
</button>
</div>
</div>
</div>
<div class="profile-page">
<div class="d-flex">
<div class="flex-fill">
<h1 class="mb-1">{{title}}</h1>
</div>
<span class="flex-spacer"></span>
<div class="align-self-center me-2 text-end"></div>
</div>
<mat-divider class="my-3"></mat-divider>
<app-list-errors [errors]="errors"></app-list-errors>
<mat-card class="mt-4">
<mat-card-content>
<div class="row mt-2">
<div class="col-lg-6 col-md-12">
<dl class="dl-horizontal mb-0">
<dt>Nom:</dt>
<dd>{{ user.lastname }}</dd>
<dt>Prénom:</dt>
<dd>{{ user.firstname }}</dd>
</dl>
</div>
<div class="col-lg-6 col-md-12">
<dl class="dl-horizontal mb-0">
<dt>Email:</dt>
<dd>{{ user.email }}</dd>
<dt>Téléphone:</dt>
<dd>{{ user.phone }}</dd>
</dl>
</div>
</div>
</mat-card-content>
</mat-card>
<mat-card>
<mat-card-header class="text-bg-primary rounded-top py-2 px-4">
<div class="flex-fill">
<mat-card-title>
Mes commandes
</mat-card-title>
</div>
<span class="flex-spacer"></span>
<div class="flex-fill"></div>
</mat-card-header>
<mat-card-content>
<table class="table table-dark table-striped table-condensed mat-elevation-z2">
<thead>
<tr role="row" class="table-dark">
<th class="align-middle w-25 ps-4">
<div class="text-left">
Date
</div>
</th>
<th class="align-middle w-25">
<div class="text-left">
Numéro
</div>
</th>
<th class="align-middle w-25">
<div class="text-left">
Statut
</div>
</th>
<th class="align-middle w-25">
<div class="text-left">
Total
</div>
</th>
</tr>
</thead>
<tbody>
<tr role="row" class="align-middle">
<td class="cell ps-4">
20/04/2025 13:37
</td>
<td class="cell">
#aa2025042013371
</td>
<td class="cell">
<span class="text-green">Terminé</span>
</td>
<td class="cell">
56.00 €
</td>
</tr>
<tr role="row" class="align-middle">
<td class="cell ps-4">
20/04/2025 13:42
</td>
<td class="cell">
#aa2025042013421
</td>
<td class="cell">
<span class="text-teal">Expédié</span>
</td>
<td class="cell">
224.70 €
</td>
</tr>
<tr role="row" class="align-middle">
<td class="cell ps-4">
20/04/2025 13:54
</td>
<td class="cell">
#aa2025042013541
</td>
<td class="cell">
<span class="text-red">Annulé</span>
</td>
<td class="cell">
128.80 €
</td>
</tr>
<tr role="row" class="align-middle">
<td class="cell ps-4">
14/06/2025 13:37
</td>
<td class="cell">
#aa2025061413371
</td>
<td class="cell">
<span class="text-cyan">En cours de préparation</span>
</td>
<td class="cell">
195.30 €
</td>
</tr>
<tr role="row" class="align-middle">
<td class="cell ps-4">
07/07/2025 14:39
</td>
<td class="cell">
#aa2025070714391
</td>
<td class="cell">
<span class="text-orange">En attente de paiement</span>
</td>
<td class="cell">
64.00 €
</td>
</tr>
</tbody>
</table>
</mat-card-content>
</mat-card>
</div>
@@ -0,0 +1,32 @@
/* Profile */
//@use 'variable' as var;
// --> background-color: var.$sidebar-footer;
.mat-mdc-card + .mat-mdc-card {
margin-top: 10px;
}
/*
/-----------------------------\
| Skydive Paracord Keychain |
|-----------------------------|
| 01-Rouge - #FF2F19 |
| 02-Orange - #FF9124 |
| 03-Jaune - #FFF135 |
| 04-Vert-fluo - #AEFC3C |
| 05-Turquoise - #4AD2C2 |
| 06-Bleu-ciel - #E2F5FF |
| 07-Bleu-clair - #1BACFF |
| 08-Bleu-foncé - #1E45CE |
| 09-Violet - #4F2AA4 |
| 10-Rose - #FC5EC0 |
|-----------------------------|
| 11-Blanc - #FFFFFF |
| 12-Beige - #D8CBA2 |
| 13-Ruby - #B21B27 |
| 14-Vert-olive - #335034 |
| 15-Bleu-navy - #35496F |
| 16-Noir - #000000 |
\-----------------------------/
*/
@@ -0,0 +1,21 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ProfileComponent } from './profile.component';
describe('ProfileComponent', () => {
let component: ProfileComponent;
let fixture: ComponentFixture<ProfileComponent>;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [ProfileComponent]
});
fixture = TestBed.createComponent(ProfileComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

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