altes backend entfenrt
@@ -1,16 +0,0 @@
|
|||||||
# Editor configuration, see https://editorconfig.org
|
|
||||||
root = true
|
|
||||||
|
|
||||||
[*]
|
|
||||||
charset = utf-8
|
|
||||||
indent_style = space
|
|
||||||
indent_size = 2
|
|
||||||
insert_final_newline = true
|
|
||||||
trim_trailing_whitespace = true
|
|
||||||
|
|
||||||
[*.ts]
|
|
||||||
quote_type = single
|
|
||||||
|
|
||||||
[*.md]
|
|
||||||
max_line_length = off
|
|
||||||
trim_trailing_whitespace = false
|
|
||||||
49
myteamwallet_frontend/.gitignore
vendored
@@ -1,49 +0,0 @@
|
|||||||
# See http://help.github.com/ignore-files/ for more about ignoring files.
|
|
||||||
|
|
||||||
# Compiled output
|
|
||||||
/dist
|
|
||||||
/tmp
|
|
||||||
/out-tsc
|
|
||||||
/bazel-out
|
|
||||||
|
|
||||||
# Node
|
|
||||||
/node_modules
|
|
||||||
npm-debug.log
|
|
||||||
yarn-error.log
|
|
||||||
|
|
||||||
# IDEs and editors
|
|
||||||
.idea/
|
|
||||||
.project
|
|
||||||
.classpath
|
|
||||||
.c9/
|
|
||||||
*.launch
|
|
||||||
.settings/
|
|
||||||
*.sublime-workspace
|
|
||||||
|
|
||||||
# Visual Studio Code
|
|
||||||
.vscode/*
|
|
||||||
!.vscode/settings.json
|
|
||||||
!.vscode/tasks.json
|
|
||||||
!.vscode/launch.json
|
|
||||||
!.vscode/extensions.json
|
|
||||||
.history/*
|
|
||||||
|
|
||||||
# Miscellaneous
|
|
||||||
/.angular/cache
|
|
||||||
.sass-cache/
|
|
||||||
/connect.lock
|
|
||||||
/coverage
|
|
||||||
/libpeerconnection.log
|
|
||||||
testem.log
|
|
||||||
/typings
|
|
||||||
|
|
||||||
# System files
|
|
||||||
.DS_Store
|
|
||||||
Thumbs.db
|
|
||||||
|
|
||||||
# Claude Code worktrees / plan tooling
|
|
||||||
/.claude
|
|
||||||
/.superpowers
|
|
||||||
|
|
||||||
# git
|
|
||||||
package-lock.json
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846
|
|
||||||
"recommendations": ["angular.ng-template"]
|
|
||||||
}
|
|
||||||
20
myteamwallet_frontend/.vscode/launch.json
vendored
@@ -1,20 +0,0 @@
|
|||||||
{
|
|
||||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
|
||||||
"version": "0.2.0",
|
|
||||||
"configurations": [
|
|
||||||
{
|
|
||||||
"name": "ng serve",
|
|
||||||
"type": "pwa-chrome",
|
|
||||||
"request": "launch",
|
|
||||||
"preLaunchTask": "npm: start",
|
|
||||||
"url": "http://localhost:4200/"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "ng test",
|
|
||||||
"type": "chrome",
|
|
||||||
"request": "launch",
|
|
||||||
"preLaunchTask": "npm: test",
|
|
||||||
"url": "http://localhost:9876/debug.html"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
3
myteamwallet_frontend/.vscode/settings.json
vendored
@@ -1,3 +0,0 @@
|
|||||||
{
|
|
||||||
"CodeGPT.apiKey": "CodeGPT Plus Beta"
|
|
||||||
}
|
|
||||||
42
myteamwallet_frontend/.vscode/tasks.json
vendored
@@ -1,42 +0,0 @@
|
|||||||
{
|
|
||||||
// For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558
|
|
||||||
"version": "2.0.0",
|
|
||||||
"tasks": [
|
|
||||||
{
|
|
||||||
"type": "npm",
|
|
||||||
"script": "start",
|
|
||||||
"isBackground": true,
|
|
||||||
"problemMatcher": {
|
|
||||||
"owner": "typescript",
|
|
||||||
"pattern": "$tsc",
|
|
||||||
"background": {
|
|
||||||
"activeOnStart": true,
|
|
||||||
"beginsPattern": {
|
|
||||||
"regexp": "(.*?)"
|
|
||||||
},
|
|
||||||
"endsPattern": {
|
|
||||||
"regexp": "bundle generation complete"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "npm",
|
|
||||||
"script": "test",
|
|
||||||
"isBackground": true,
|
|
||||||
"problemMatcher": {
|
|
||||||
"owner": "typescript",
|
|
||||||
"pattern": "$tsc",
|
|
||||||
"background": {
|
|
||||||
"activeOnStart": true,
|
|
||||||
"beginsPattern": {
|
|
||||||
"regexp": "(.*?)"
|
|
||||||
},
|
|
||||||
"endsPattern": {
|
|
||||||
"regexp": "bundle generation complete"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
# MyteamwalletFrontend
|
|
||||||
|
|
||||||
This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 15.0.1.
|
|
||||||
|
|
||||||
## Development server
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
## Code scaffolding
|
|
||||||
|
|
||||||
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`.
|
|
||||||
|
|
||||||
## Build
|
|
||||||
|
|
||||||
Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory.
|
|
||||||
|
|
||||||
## 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.
|
|
||||||
@@ -1,135 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
|
||||||
"version": 1,
|
|
||||||
"newProjectRoot": "projects",
|
|
||||||
"projects": {
|
|
||||||
"myteamwallet_frontend": {
|
|
||||||
"projectType": "application",
|
|
||||||
"schematics": {
|
|
||||||
"@schematics/angular:component": {
|
|
||||||
"style": "scss"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"root": "",
|
|
||||||
"sourceRoot": "src",
|
|
||||||
"prefix": "app",
|
|
||||||
"architect": {
|
|
||||||
"build": {
|
|
||||||
"builder": "@angular-devkit/build-angular:browser",
|
|
||||||
"options": {
|
|
||||||
"outputPath": "dist",
|
|
||||||
"index": "src/index.html",
|
|
||||||
"main": "src/main.ts",
|
|
||||||
"polyfills": [
|
|
||||||
"zone.js"
|
|
||||||
],
|
|
||||||
"statsJson": true,
|
|
||||||
"tsConfig": "tsconfig.app.json",
|
|
||||||
"inlineStyleLanguage": "scss",
|
|
||||||
"assets": [
|
|
||||||
"src/favicon.ico",
|
|
||||||
"src/assets",
|
|
||||||
"src/manifest.webmanifest"
|
|
||||||
],
|
|
||||||
"styles": [
|
|
||||||
"src/styles.scss"
|
|
||||||
],
|
|
||||||
"scripts": [],
|
|
||||||
"serviceWorker": true,
|
|
||||||
"ngswConfigPath": "ngsw-config.json"
|
|
||||||
},
|
|
||||||
"configurations": {
|
|
||||||
"production": {
|
|
||||||
"budgets": [
|
|
||||||
{
|
|
||||||
"type": "initial",
|
|
||||||
"maximumWarning": "500kb",
|
|
||||||
"maximumError": "1mb"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "anyComponentStyle",
|
|
||||||
"maximumWarning": "2kb",
|
|
||||||
"maximumError": "4kb"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"outputHashing": "all",
|
|
||||||
"fileReplacements": [
|
|
||||||
{
|
|
||||||
"replace": "src/environments/environment.ts",
|
|
||||||
"with": "src/environments/environment.prod.ts"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"development": {
|
|
||||||
"buildOptimizer": false,
|
|
||||||
"optimization": false,
|
|
||||||
"vendorChunk": true,
|
|
||||||
"extractLicenses": false,
|
|
||||||
"sourceMap": true,
|
|
||||||
"namedChunks": true,
|
|
||||||
"fileReplacements": [
|
|
||||||
{
|
|
||||||
"replace": "src/environments/environment.ts",
|
|
||||||
"with": "src/environments/environment.dev.ts"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"remote": {
|
|
||||||
"buildOptimizer": false,
|
|
||||||
"optimization": false,
|
|
||||||
"vendorChunk": true,
|
|
||||||
"extractLicenses": false,
|
|
||||||
"sourceMap": true,
|
|
||||||
"namedChunks": true,
|
|
||||||
"fileReplacements": []
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"defaultConfiguration": "production"
|
|
||||||
},
|
|
||||||
"serve": {
|
|
||||||
"builder": "@angular-devkit/build-angular:dev-server",
|
|
||||||
"configurations": {
|
|
||||||
"production": {
|
|
||||||
"buildTarget": "myteamwallet_frontend:build:production"
|
|
||||||
},
|
|
||||||
"development": {
|
|
||||||
"buildTarget": "myteamwallet_frontend:build:development"
|
|
||||||
},
|
|
||||||
"remote": {
|
|
||||||
"buildTarget": "myteamwallet_frontend:build:remote"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"defaultConfiguration": "development"
|
|
||||||
},
|
|
||||||
"extract-i18n": {
|
|
||||||
"builder": "@angular-devkit/build-angular:extract-i18n",
|
|
||||||
"options": {
|
|
||||||
"buildTarget": "myteamwallet_frontend: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"
|
|
||||||
],
|
|
||||||
"styles": [
|
|
||||||
"src/styles.scss"
|
|
||||||
],
|
|
||||||
"scripts": []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"cli": {
|
|
||||||
"analytics": false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,374 +0,0 @@
|
|||||||
# Design-System-Fundament Implementation Plan
|
|
||||||
|
|
||||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
||||||
|
|
||||||
**Goal:** Legt die technische und visuelle Basis für das TeamWallet-Redesign: Design-Tokens, ein eigenes Material-3-Theme (fintech-modern, Indigo/Grün), ein konsistentes Icon-System und ein bereinigtes Notification-System — ohne die bestehenden Bildschirme inhaltlich umzubauen (das folgt in Folge-Plänen für Public-Übersicht, Dashboard/Team-Workspace, Auth-Flows).
|
|
||||||
|
|
||||||
**Architecture:** Zwei neue SCSS-Partials (`src/styles/_tokens.scss`, `src/styles/_theme.scss`) werden aus `src/styles.scss` eingebunden. Die Angular-Material-Theming-API (`mat.define-theme`, installiert in `node_modules/@angular/material/core/theming/_definition.scss`) erzeugt das Theme aus vordefinierten M3-Paletten (`mat.$blue-palette` als Primary, `mat.$spring-green-palette` als Tertiary) statt des Stock-Themes `azure-blue`. Reine CSS-Custom-Properties (nicht Sass-Variablen) tragen Spacing/Radius/Elevation/Balance-Farben, damit sie direkt in Komponenten-Templates/-Styles nutzbar sind, auch außerhalb von Sass-Kontext.
|
|
||||||
|
|
||||||
**Tech Stack:** Angular 18.1, Angular Material 18.1 (M3-Theming-API), SCSS, Angular CLI Builder (`@angular-devkit/build-angular:browser`), Karma/Jasmine.
|
|
||||||
|
|
||||||
## Global Constraints
|
|
||||||
|
|
||||||
- Kein Dark Mode in diesem Plan — Tokens müssen aber so benannt/strukturiert sein, dass ein späteres Dark-Theme sie überschreiben kann (semantische Namen, keine rohen Paletten-Referenzen in Komponenten).
|
|
||||||
- Keine neuen Abhängigkeiten hinzufügen; stattdessen wird die ungenutzte Abhängigkeit `@ngxpert/hot-toast` entfernt.
|
|
||||||
- Bestehende deutsche UI-Texte bleiben unverändert (i18n-Audit ist ein separater Folge-Plan).
|
|
||||||
- Nach jedem Task muss `npm run build` (aus `myteamwallet_frontend/`) fehlerfrei durchlaufen.
|
|
||||||
- Angular Material 18 M3-Theming bietet **keine** Funktion, aus einem beliebigen Hex-Wert eine Palette zu generieren (verifiziert: `_palettes.scss` enthält nur die zwölf vordefinierten Paletten `red/green/blue/yellow/cyan/magenta/orange/chartreuse/spring-green/azure/violet/rose`). Primary/Tertiary werden daher aus diesen vordefinierten Paletten gewählt, nicht aus Wunsch-Hexcodes.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Task 1: Design-Tokens (Spacing, Radius, Elevation, Balance-Farben)
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Create: `myteamwallet_frontend/src/styles/_tokens.scss`
|
|
||||||
- Modify: `myteamwallet_frontend/src/styles.scss`
|
|
||||||
|
|
||||||
**Interfaces:**
|
|
||||||
- Produces: CSS-Custom-Properties auf `:root` — `--tw-space-{1,2,3,4,6,8}`, `--tw-radius-{sm,md,lg}`, `--tw-elevation-{1,2}`, `--tw-balance-positive`, `--tw-balance-negative`, `--tw-balance-neutral`, `--tw-surface-bg`, `--tw-surface-card`. Alle Folge-Tasks/-Pläne referenzieren ausschließlich diese Namen, nie rohe Hex-Werte.
|
|
||||||
|
|
||||||
- [ ] **Step 1: `_tokens.scss` anlegen**
|
|
||||||
|
|
||||||
```scss
|
|
||||||
// myteamwallet_frontend/src/styles/_tokens.scss
|
|
||||||
:root {
|
|
||||||
// Spacing (8px-Grid)
|
|
||||||
--tw-space-1: 4px;
|
|
||||||
--tw-space-2: 8px;
|
|
||||||
--tw-space-3: 12px;
|
|
||||||
--tw-space-4: 16px;
|
|
||||||
--tw-space-6: 24px;
|
|
||||||
--tw-space-8: 32px;
|
|
||||||
|
|
||||||
// Radius
|
|
||||||
--tw-radius-sm: 8px;
|
|
||||||
--tw-radius-md: 12px;
|
|
||||||
--tw-radius-lg: 16px;
|
|
||||||
|
|
||||||
// Elevation (ersetzt die bisher wiederholte box-shadow-Deklaration)
|
|
||||||
--tw-elevation-1: 0 1px 2px rgba(15, 23, 42, 0.06), 0 1px 3px rgba(15, 23, 42, 0.1);
|
|
||||||
--tw-elevation-2: 0 2px 4px rgba(15, 23, 42, 0.06), 0 4px 8px rgba(15, 23, 42, 0.1);
|
|
||||||
|
|
||||||
// Semantische Saldo-Farben (fintech-modern: gedämpftes Grün/Rot statt Signalfarben)
|
|
||||||
--tw-balance-positive: #1b8a5a;
|
|
||||||
--tw-balance-negative: #c4351c;
|
|
||||||
--tw-balance-neutral: #64748b;
|
|
||||||
|
|
||||||
// Flächen
|
|
||||||
--tw-surface-bg: #f7f8fa;
|
|
||||||
--tw-surface-card: #ffffff;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 2: In `styles.scss` einbinden und bestehende Hardcodes ersetzen**
|
|
||||||
|
|
||||||
In `myteamwallet_frontend/src/styles.scss` ganz oben ergänzen:
|
|
||||||
|
|
||||||
```scss
|
|
||||||
@use './styles/tokens';
|
|
||||||
```
|
|
||||||
|
|
||||||
Danach folgende bestehende Stellen in derselben Datei ersetzen:
|
|
||||||
|
|
||||||
```scss
|
|
||||||
// vorher: body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; background-color: #fafafa;}
|
|
||||||
body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; background-color: var(--tw-surface-bg); }
|
|
||||||
```
|
|
||||||
|
|
||||||
```scss
|
|
||||||
// vorher in .card:
|
|
||||||
// box-shadow: 0 2px 1px -1px #0003, 0 1px 1px #00000024, 0 1px 3px #0000001f;
|
|
||||||
.card {
|
|
||||||
padding: var(--tw-space-3);
|
|
||||||
box-shadow: var(--tw-elevation-1);
|
|
||||||
margin: var(--tw-space-1);
|
|
||||||
background-color: var(--tw-surface-card);
|
|
||||||
border-radius: var(--tw-radius-md);
|
|
||||||
|
|
||||||
&.flat {
|
|
||||||
background-color: transparent;
|
|
||||||
box-shadow: none;
|
|
||||||
outline: #ddd solid 1px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
```scss
|
|
||||||
// vorher: background-color: rgb(148, 16, 16) !important;
|
|
||||||
.snackbar_error > .mdc-snackbar__surface {
|
|
||||||
background-color: var(--tw-balance-negative) !important;
|
|
||||||
|
|
||||||
.mat-mdc-snack-bar-label {
|
|
||||||
color: white !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 3: Build verifizieren**
|
|
||||||
|
|
||||||
Run: `cd myteamwallet_frontend && npm run build`
|
|
||||||
Expected: Build erfolgreich, keine Sass-/Compile-Fehler.
|
|
||||||
|
|
||||||
- [ ] **Step 4: Commit**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git add myteamwallet_frontend/src/styles/_tokens.scss myteamwallet_frontend/src/styles.scss
|
|
||||||
git commit -m "feat(design): add design tokens and wire into global styles"
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Task 2: Eigenes Material-3-Theme statt Stock-Theme
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Create: `myteamwallet_frontend/src/styles/_theme.scss`
|
|
||||||
- Modify: `myteamwallet_frontend/src/styles.scss`
|
|
||||||
- Modify: `myteamwallet_frontend/angular.json:35` (build-Styles), `myteamwallet_frontend/angular.json:125` (test-Styles)
|
|
||||||
- Modify: `myteamwallet_frontend/src/index.html`
|
|
||||||
|
|
||||||
**Interfaces:**
|
|
||||||
- Consumes: nichts aus Task 1 direkt (unabhängiges Partial), wird aber in derselben `styles.scss` eingebunden.
|
|
||||||
- Produces: Sass-Variable `theme.$teamwallet-theme`, die alle Material-Komponentenstyles erzeugt. Spätere Pläne fügen hier bei Bedarf `mat.theme-overrides()` hinzu, ändern aber nicht den Namen `$teamwallet-theme`.
|
|
||||||
|
|
||||||
- [ ] **Step 1: `_theme.scss` anlegen**
|
|
||||||
|
|
||||||
```scss
|
|
||||||
// myteamwallet_frontend/src/styles/_theme.scss
|
|
||||||
@use '@angular/material' as mat;
|
|
||||||
|
|
||||||
$teamwallet-theme: mat.define-theme((
|
|
||||||
color: (
|
|
||||||
theme-type: light,
|
|
||||||
primary: mat.$blue-palette,
|
|
||||||
tertiary: mat.$spring-green-palette,
|
|
||||||
),
|
|
||||||
typography: (
|
|
||||||
brand-family: 'Inter, Roboto, "Helvetica Neue", sans-serif',
|
|
||||||
plain-family: 'Inter, Roboto, "Helvetica Neue", sans-serif',
|
|
||||||
),
|
|
||||||
density: (
|
|
||||||
scale: 0,
|
|
||||||
),
|
|
||||||
));
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 2: Theme in `styles.scss` einbinden (ersetzt Stock-Theme-Import)**
|
|
||||||
|
|
||||||
Am Anfang von `myteamwallet_frontend/src/styles.scss` ergänzen (vor den bestehenden Regeln):
|
|
||||||
|
|
||||||
```scss
|
|
||||||
@use '@angular/material' as mat;
|
|
||||||
@use './styles/theme';
|
|
||||||
|
|
||||||
@include mat.core();
|
|
||||||
@include mat.all-component-themes(theme.$teamwallet-theme);
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 3: Stock-Theme aus `angular.json` entfernen**
|
|
||||||
|
|
||||||
In `myteamwallet_frontend/angular.json` im `build`-Target die Zeile entfernen:
|
|
||||||
|
|
||||||
```json
|
|
||||||
"@angular/material/prebuilt-themes/azure-blue.css",
|
|
||||||
```
|
|
||||||
|
|
||||||
Im `test`-Target die Zeile entfernen:
|
|
||||||
|
|
||||||
```json
|
|
||||||
"@angular/material/prebuilt-themes/cyan-orange.css",
|
|
||||||
```
|
|
||||||
|
|
||||||
In beiden Targets bleibt `"src/styles.scss"` als einziger globaler Stylesheet-Eintrag stehen.
|
|
||||||
|
|
||||||
- [ ] **Step 4: Google-Font auf Inter umstellen**
|
|
||||||
|
|
||||||
In `myteamwallet_frontend/src/index.html` die bestehende Roboto-Zeile ersetzen:
|
|
||||||
|
|
||||||
```html
|
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 5: Build und Tests verifizieren**
|
|
||||||
|
|
||||||
Run: `cd myteamwallet_frontend && npm run build`
|
|
||||||
Expected: Build erfolgreich, Material-Komponenten (Buttons, Toolbar, Cards) werden aus dem neuen Theme gestylt statt aus `azure-blue.css`.
|
|
||||||
|
|
||||||
Run: `cd myteamwallet_frontend && npm test -- --watch=false`
|
|
||||||
Expected: Bestehende Karma-Suite läuft weiterhin durch (keine Test-Regression durch den Theme-Wechsel).
|
|
||||||
|
|
||||||
- [ ] **Step 6: Commit**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git add myteamwallet_frontend/src/styles/_theme.scss myteamwallet_frontend/src/styles.scss myteamwallet_frontend/angular.json myteamwallet_frontend/src/index.html
|
|
||||||
git commit -m "feat(design): replace stock Material azure-blue theme with custom M3 theme"
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Task 3: Icon-System auf Material Symbols umstellen, PNG-Icon-Klassen entfernen
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Modify: `myteamwallet_frontend/src/index.html`
|
|
||||||
- Modify: `myteamwallet_frontend/src/app/app.component.ts`
|
|
||||||
- Modify: `myteamwallet_frontend/src/app/modules/home/dashboard/dashboard.component.ts`
|
|
||||||
- Modify: `myteamwallet_frontend/src/app/modules/home/dashboard/dashboard.component.html`
|
|
||||||
- Modify: `myteamwallet_frontend/src/styles.scss`
|
|
||||||
- Delete: `myteamwallet_frontend/src/assets/edit.png`, `myteamwallet_frontend/src/assets/dashboard.png`, `myteamwallet_frontend/src/assets/tacho.png`
|
|
||||||
|
|
||||||
**Interfaces:**
|
|
||||||
- Consumes: nichts.
|
|
||||||
- Produces: `<mat-icon>`-basierte Icons überall; keine PNG-Icon-Klassen (`icon`, `icon_edit`, `icon_dashboard`, `icon_tacho`) mehr im Code. Verifiziert: diese Klassen werden ausschließlich in `dashboard.component.html` verwendet (`icon_tacho`, `icon_edit`; `icon_dashboard` ist bereits toter Code).
|
|
||||||
|
|
||||||
- [ ] **Step 1: Material Symbols statt Material Icons laden**
|
|
||||||
|
|
||||||
In `myteamwallet_frontend/src/index.html` die Material-Icons-Zeile ersetzen:
|
|
||||||
|
|
||||||
```html
|
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200&display=swap" rel="stylesheet">
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 2: Default-Fontset der App auf Material Symbols umstellen**
|
|
||||||
|
|
||||||
In `myteamwallet_frontend/src/app/app.component.ts`:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
import { Component } from '@angular/core';
|
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
|
||||||
import { environment } from './../environments/environment';
|
|
||||||
import { SwUpdate } from '@angular/service-worker';
|
|
||||||
import { MatIconRegistry } from '@angular/material/icon';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-root',
|
|
||||||
templateUrl: './app.component.html',
|
|
||||||
styleUrls: ['./app.component.scss']
|
|
||||||
})
|
|
||||||
export class AppComponent {
|
|
||||||
version = environment.appVersion;
|
|
||||||
|
|
||||||
constructor(translate: TranslateService, updates: SwUpdate, iconRegistry: MatIconRegistry) {
|
|
||||||
iconRegistry.setDefaultFontSetClass('material-symbols-outlined');
|
|
||||||
translate.setDefaultLang('de');
|
|
||||||
translate.use('de');
|
|
||||||
this.update(updates);
|
|
||||||
}
|
|
||||||
// ... rest unverändert
|
|
||||||
```
|
|
||||||
|
|
||||||
(Restlichen Methodenkörper `update()` unverändert lassen.)
|
|
||||||
|
|
||||||
- [ ] **Step 3: Dashboard-Icons auf `mat-icon` umstellen**
|
|
||||||
|
|
||||||
In `myteamwallet_frontend/src/app/modules/home/dashboard/dashboard.component.ts` `MatIconModule` importieren:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
import { MatIconModule } from '@angular/material/icon';
|
|
||||||
// ...
|
|
||||||
@Component({
|
|
||||||
selector: 'app-dashboard',
|
|
||||||
templateUrl: './dashboard.component.html',
|
|
||||||
styleUrls: ['./dashboard.component.scss'],
|
|
||||||
standalone: true,
|
|
||||||
imports: [ CommonModule, TranslateModule, MatButtonModule, MatIconModule ]
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
In `myteamwallet_frontend/src/app/modules/home/dashboard/dashboard.component.html` ersetzen:
|
|
||||||
|
|
||||||
```html
|
|
||||||
<!-- vorher -->
|
|
||||||
<div class="icons">
|
|
||||||
<div class="icon icon_tacho" matRipple (click)="onTeamClick(p.team)"></div>
|
|
||||||
<div class="icon icon_edit" matRipple (click)="onTeamClick(p.team)"></div>
|
|
||||||
</div>
|
|
||||||
```
|
|
||||||
|
|
||||||
```html
|
|
||||||
<!-- nachher -->
|
|
||||||
<div class="icons">
|
|
||||||
<button mat-icon-button (click)="onTeamClick(p.team)" aria-label="Team-Details öffnen">
|
|
||||||
<mat-icon>speed</mat-icon>
|
|
||||||
</button>
|
|
||||||
<button mat-icon-button (click)="onTeamClick(p.team)" aria-label="Team bearbeiten">
|
|
||||||
<mat-icon>edit</mat-icon>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 4: Tote Icon-Klassen aus `styles.scss` entfernen**
|
|
||||||
|
|
||||||
In `myteamwallet_frontend/src/styles.scss` den gesamten Block entfernen:
|
|
||||||
|
|
||||||
```scss
|
|
||||||
.icon { ... }
|
|
||||||
.icon_edit { ... }
|
|
||||||
.icon_dashboard { ... }
|
|
||||||
.icon_tacho { ... }
|
|
||||||
```
|
|
||||||
|
|
||||||
(Alle vier Regeln inkl. der `.icon:hover`-Verschachtelung löschen — keine andere Datei referenziert diese Klassen mehr.)
|
|
||||||
|
|
||||||
- [ ] **Step 5: Ungenutzte PNG-Assets löschen**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git rm myteamwallet_frontend/src/assets/edit.png myteamwallet_frontend/src/assets/dashboard.png myteamwallet_frontend/src/assets/tacho.png
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 6: Build verifizieren und visuell prüfen**
|
|
||||||
|
|
||||||
Run: `cd myteamwallet_frontend && npm run build`
|
|
||||||
Expected: Build erfolgreich, keine fehlenden Asset-Referenzen.
|
|
||||||
|
|
||||||
Run: `cd myteamwallet_frontend && npm start`, Dashboard im Browser öffnen (`http://localhost:4200/dashboard`, eingeloggt).
|
|
||||||
Expected: Statt der beiden PNG-Icons erscheinen die Material-Symbols-Icons „speed“ und „edit“, klickbar wie zuvor.
|
|
||||||
|
|
||||||
- [ ] **Step 7: Commit**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git add myteamwallet_frontend/src/index.html myteamwallet_frontend/src/app/app.component.ts myteamwallet_frontend/src/app/modules/home/dashboard/dashboard.component.ts myteamwallet_frontend/src/app/modules/home/dashboard/dashboard.component.html myteamwallet_frontend/src/styles.scss
|
|
||||||
git commit -m "feat(design): migrate to Material Symbols, remove PNG icon classes"
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Task 4: Notification-System bereinigen
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Modify: `myteamwallet_frontend/package.json`
|
|
||||||
|
|
||||||
**Interfaces:**
|
|
||||||
- Consumes: `--tw-balance-negative`-Token aus Task 1 (bereits in `.snackbar_error` verdrahtet).
|
|
||||||
- Produces: keine neuen Symbole — reine Bereinigung.
|
|
||||||
|
|
||||||
Verifiziert: `@ngxpert/hot-toast` ist in `package.json` als Abhängigkeit deklariert, wird aber im gesamten `src/`-Verzeichnis nirgends importiert (`HotToastService`/`@ngxpert/hot-toast` liefert null Treffer). Es gibt also kein zweites, konkurrierendes Toast-System im Code — nur eine tote Abhängigkeit. Die tatsächliche Benachrichtigung läuft ausschließlich über `MatSnackBar` (3 Aufrufstellen, alle in `team-details.component.ts`), deren Fehler-Variante bereits in Task 1 auf den neuen Token umgestellt wurde.
|
|
||||||
|
|
||||||
- [ ] **Step 1: Ungenutzte Abhängigkeit entfernen**
|
|
||||||
|
|
||||||
In `myteamwallet_frontend/package.json` die Zeile aus `dependencies` entfernen:
|
|
||||||
|
|
||||||
```json
|
|
||||||
"@ngxpert/hot-toast": "3.0.0",
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 2: Lockfile aktualisieren**
|
|
||||||
|
|
||||||
Run: `cd myteamwallet_frontend && npm install`
|
|
||||||
Expected: `package-lock.json` aktualisiert sich, `@ngxpert/hot-toast` verschwindet aus dem Dependency-Baum, keine anderen Pakete werden unerwartet verändert.
|
|
||||||
|
|
||||||
- [ ] **Step 3: Build verifizieren**
|
|
||||||
|
|
||||||
Run: `cd myteamwallet_frontend && npm run build`
|
|
||||||
Expected: Build weiterhin erfolgreich (bestätigt, dass das Paket wirklich ungenutzt war).
|
|
||||||
|
|
||||||
- [ ] **Step 4: Commit**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git add myteamwallet_frontend/package.json myteamwallet_frontend/package-lock.json
|
|
||||||
git commit -m "chore(deps): remove unused @ngxpert/hot-toast dependency"
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Hinweis für Folge-Pläne
|
|
||||||
|
|
||||||
Die Wrapper-Komponenten-Bibliothek (`BalanceDisplay`, `PageHeader`, `SectionCard`, `EmptyState`, `StatTile` aus dem Gesamtplan) wird bewusst **nicht** in diesem Fundament-Plan gebaut, sondern erst in dem Folge-Plan, der ihren ersten echten Verwendungsort umbaut (Public-Team-Übersicht bzw. Dashboard/Team-Workspace) — YAGNI: eine Komponenten-Bibliothek ohne Konsumenten lässt sich nicht sinnvoll gegen echte Anforderungen entwerfen. Dieser Fundament-Plan liefert die Basis (Tokens, Theme, Icons, saubere Notifications), auf der diese Komponenten aufbauen.
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "./node_modules/@angular/service-worker/config/schema.json",
|
|
||||||
"index": "/index.html",
|
|
||||||
"assetGroups": [
|
|
||||||
{
|
|
||||||
"name": "app",
|
|
||||||
"installMode": "prefetch",
|
|
||||||
"resources": {
|
|
||||||
"files": [
|
|
||||||
"/favicon.ico",
|
|
||||||
"/index.html",
|
|
||||||
"/manifest.webmanifest",
|
|
||||||
"/*.css",
|
|
||||||
"/*.js"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "assets",
|
|
||||||
"installMode": "lazy",
|
|
||||||
"updateMode": "prefetch",
|
|
||||||
"resources": {
|
|
||||||
"files": [
|
|
||||||
"/**/*.(svg|cur|jpg|jpeg|png|apng|webp|avif|gif|otf|ttf|woff|woff2)"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "myteamwallet-frontend",
|
|
||||||
"version": "1.0.49",
|
|
||||||
"scripts": {
|
|
||||||
"ng": "ng",
|
|
||||||
"start": "ng serve",
|
|
||||||
"start:remote": "ng serve --configuration=remote",
|
|
||||||
"build": "ng build",
|
|
||||||
"watch": "ng build --watch --configuration development",
|
|
||||||
"test": "ng test",
|
|
||||||
"patch": "npm --no-git-tag-version version patch",
|
|
||||||
"post_patch": "git add package.json"
|
|
||||||
},
|
|
||||||
"pre-commit": [
|
|
||||||
"patch",
|
|
||||||
"post_patch"
|
|
||||||
],
|
|
||||||
"private": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@angular/animations": "^18.1.4",
|
|
||||||
"@angular/cdk": "^18.1.4",
|
|
||||||
"@angular/common": "^18.1.4",
|
|
||||||
"@angular/compiler": "^18.1.4",
|
|
||||||
"@angular/core": "^18.1.4",
|
|
||||||
"@angular/forms": "^18.1.4",
|
|
||||||
"@angular/material": "^18.1.4",
|
|
||||||
"@angular/platform-browser": "^18.1.4",
|
|
||||||
"@angular/platform-browser-dynamic": "^18.1.4",
|
|
||||||
"@angular/router": "^18.1.4",
|
|
||||||
"@angular/service-worker": "^18.1.4",
|
|
||||||
"@ngx-translate/core": "^15.0.0",
|
|
||||||
"@ngx-translate/http-loader": "^8.0.0",
|
|
||||||
"rxjs": "~7.5.0",
|
|
||||||
"tslib": "^2.3.0",
|
|
||||||
"zone.js": "~0.14.8"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@angular-devkit/build-angular": "^18.1.4",
|
|
||||||
"@angular/cli": "~18.1.4",
|
|
||||||
"@angular/compiler-cli": "^18.1.4",
|
|
||||||
"@types/jasmine": "~4.3.0",
|
|
||||||
"jasmine-core": "~4.5.0",
|
|
||||||
"karma": "~6.4.0",
|
|
||||||
"karma-chrome-launcher": "~3.1.0",
|
|
||||||
"karma-coverage": "~2.2.0",
|
|
||||||
"karma-jasmine": "~5.1.0",
|
|
||||||
"karma-jasmine-html-reporter": "~2.0.0",
|
|
||||||
"pre-commit": "^1.2.2",
|
|
||||||
"typescript": "~5.4.5"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 2.3 KiB |
@@ -1,2 +0,0 @@
|
|||||||
<div class="app_version">{{version}}</div>
|
|
||||||
<router-outlet></router-outlet>
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
.app_version{
|
|
||||||
position: absolute;
|
|
||||||
left: 0px;
|
|
||||||
bottom: 0px;
|
|
||||||
font-size: 0.6rem;
|
|
||||||
line-height: 0.6rem;
|
|
||||||
color: rgb(190, 190, 190);
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
import { TestBed } from '@angular/core/testing';
|
|
||||||
import { RouterTestingModule } from '@angular/router/testing';
|
|
||||||
import { AppComponent } from './app.component';
|
|
||||||
|
|
||||||
describe('AppComponent', () => {
|
|
||||||
beforeEach(async () => {
|
|
||||||
await TestBed.configureTestingModule({
|
|
||||||
imports: [
|
|
||||||
RouterTestingModule
|
|
||||||
],
|
|
||||||
declarations: [
|
|
||||||
AppComponent
|
|
||||||
],
|
|
||||||
}).compileComponents();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should create the app', () => {
|
|
||||||
const fixture = TestBed.createComponent(AppComponent);
|
|
||||||
const app = fixture.componentInstance;
|
|
||||||
expect(app).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it(`should have as title 'myteamwallet_frontend'`, () => {
|
|
||||||
const fixture = TestBed.createComponent(AppComponent);
|
|
||||||
const app = fixture.componentInstance;
|
|
||||||
expect(app.title).toEqual('myteamwallet_frontend');
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
import { Component } from '@angular/core';
|
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
|
||||||
import { environment } from './../environments/environment';
|
|
||||||
import { SwUpdate } from '@angular/service-worker';
|
|
||||||
import { MatIconRegistry } from '@angular/material/icon';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-root',
|
|
||||||
templateUrl: './app.component.html',
|
|
||||||
styleUrls: ['./app.component.scss']
|
|
||||||
})
|
|
||||||
export class AppComponent {
|
|
||||||
version = environment.appVersion;
|
|
||||||
|
|
||||||
constructor(translate: TranslateService, updates: SwUpdate, iconRegistry: MatIconRegistry) {
|
|
||||||
iconRegistry.setDefaultFontSetClass('material-symbols-outlined');
|
|
||||||
translate.setDefaultLang('de');
|
|
||||||
translate.use('de');
|
|
||||||
this.update(updates);
|
|
||||||
}
|
|
||||||
|
|
||||||
update(updates: SwUpdate) {
|
|
||||||
console.log("Checking for updates: ", updates.isEnabled);
|
|
||||||
if (!updates.isEnabled) { return; }
|
|
||||||
|
|
||||||
updates.versionUpdates.subscribe(evt => {
|
|
||||||
|
|
||||||
switch (evt.type) {
|
|
||||||
case 'VERSION_DETECTED':
|
|
||||||
console.log(`Downloading new app version: ${evt.version.hash}`);
|
|
||||||
break;
|
|
||||||
case 'VERSION_READY':
|
|
||||||
console.log(`Current app version: ${evt.currentVersion.hash}`);
|
|
||||||
console.log(`New app version ready for use: ${evt.latestVersion.hash}`);
|
|
||||||
window.location.reload();
|
|
||||||
break;
|
|
||||||
case 'VERSION_INSTALLATION_FAILED':
|
|
||||||
console.log(`Failed to install app version '${evt.version.hash}': ${evt.error}`);
|
|
||||||
break;
|
|
||||||
case 'NO_NEW_VERSION_DETECTED':
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
import { NgModule } from '@angular/core';
|
|
||||||
import { BrowserModule } from '@angular/platform-browser';
|
|
||||||
|
|
||||||
import { AppComponent } from './app.component';
|
|
||||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
|
||||||
|
|
||||||
import { LOCALE_ID, isDevMode} from '@angular/core';
|
|
||||||
import localeDe from '@angular/common/locales/de';
|
|
||||||
import localeDeExtra from '@angular/common/locales/extra/de';
|
|
||||||
import { registerLocaleData } from '@angular/common';
|
|
||||||
import { HttpClient, HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
|
|
||||||
import { AuthInterceptorService } from './core/interceptor/auth.interceptor';
|
|
||||||
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
|
||||||
import { CustomTranslateLoader } from './core/translate/translate-loader';
|
|
||||||
import { provideRouter, RouterModule } from '@angular/router';
|
|
||||||
import { routes } from './app.routes';
|
|
||||||
import { ServiceWorkerModule } from '@angular/service-worker';
|
|
||||||
registerLocaleData(localeDe, localeDeExtra);
|
|
||||||
|
|
||||||
@NgModule({
|
|
||||||
declarations: [
|
|
||||||
AppComponent
|
|
||||||
],
|
|
||||||
imports: [
|
|
||||||
BrowserModule,
|
|
||||||
RouterModule,
|
|
||||||
BrowserAnimationsModule,
|
|
||||||
HttpClientModule,
|
|
||||||
TranslateModule.forRoot({
|
|
||||||
loader: {
|
|
||||||
provide: TranslateLoader,
|
|
||||||
useClass: CustomTranslateLoader,
|
|
||||||
deps: [HttpClient],
|
|
||||||
},
|
|
||||||
defaultLanguage: 'de',
|
|
||||||
}),
|
|
||||||
ServiceWorkerModule.register('ngsw-worker.js', {
|
|
||||||
enabled: !isDevMode(),
|
|
||||||
// Register the ServiceWorker as soon as the application is stable
|
|
||||||
// or after 30 seconds (whichever comes first).
|
|
||||||
registrationStrategy: 'registerWhenStable:30000'
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
providers: [
|
|
||||||
{ provide: LOCALE_ID, useValue: 'de' },
|
|
||||||
{
|
|
||||||
provide: HTTP_INTERCEPTORS,
|
|
||||||
useClass: AuthInterceptorService,
|
|
||||||
multi: true
|
|
||||||
},
|
|
||||||
provideRouter(routes)
|
|
||||||
],
|
|
||||||
bootstrap: [AppComponent]
|
|
||||||
})
|
|
||||||
export class AppModule { }
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import { Routes } from '@angular/router';
|
|
||||||
import { homeRoutes } from './modules/home/home.routes';
|
|
||||||
import { authRoutes } from './modules/auth/auth.routes';
|
|
||||||
|
|
||||||
export const routes: Routes = [
|
|
||||||
{
|
|
||||||
path: '', children: homeRoutes
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'auth', children: authRoutes
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'password-change/:hash', loadComponent: () => import('./modules/auth/password-change/password-change.component').then(m => m.PasswordChangeComponent)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'teams', loadChildren: () => import('./modules/teams/teams.module').then(m => m.TeamsModule)
|
|
||||||
}
|
|
||||||
];
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
import { Injectable } from '@angular/core';
|
|
||||||
import { ActivatedRoute, ActivatedRouteSnapshot, Route, Router, RouterStateSnapshot, UrlSegment, UrlTree } from '@angular/router';
|
|
||||||
import { Observable } from 'rxjs';
|
|
||||||
import { AuthService } from 'src/app/modules/auth.service';
|
|
||||||
|
|
||||||
|
|
||||||
@Injectable({
|
|
||||||
providedIn: 'root'
|
|
||||||
})
|
|
||||||
export class AuthGuard {
|
|
||||||
constructor(private router: Router, private activatedRoute: ActivatedRoute, private authService: AuthService) {}
|
|
||||||
|
|
||||||
private async check(): Promise<boolean> {
|
|
||||||
console.log("checking")
|
|
||||||
if (this.authService.isLoggedIn()) { return true; }
|
|
||||||
const success = await this.authService.loginFromLocalStorage();
|
|
||||||
if (success) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.router.navigate(['/auth']).then();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
canActivate(
|
|
||||||
route: ActivatedRouteSnapshot,
|
|
||||||
state: RouterStateSnapshot): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
|
|
||||||
return this.check();
|
|
||||||
}
|
|
||||||
canActivateChild(
|
|
||||||
childRoute: ActivatedRouteSnapshot,
|
|
||||||
state: RouterStateSnapshot): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
|
|
||||||
return this.check();
|
|
||||||
}
|
|
||||||
canLoad(): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
|
|
||||||
return this.check();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent, HttpErrorResponse } from '@angular/common/http';
|
|
||||||
import { Injectable } from '@angular/core';
|
|
||||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
|
||||||
import { Observable, catchError, throwError } from 'rxjs';
|
|
||||||
import { AuthService } from 'src/app/modules/auth.service';
|
|
||||||
|
|
||||||
@Injectable({
|
|
||||||
providedIn: 'root'
|
|
||||||
})
|
|
||||||
export class AuthInterceptorService implements HttpInterceptor {
|
|
||||||
constructor(private authService: AuthService, private snackBar: MatSnackBar) {}
|
|
||||||
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
|
|
||||||
const token = this.authService.getAuthToken();
|
|
||||||
|
|
||||||
if (token) {
|
|
||||||
// If we have a token, we set it to the header
|
|
||||||
request = request.clone({
|
|
||||||
setHeaders: {Authorization: `Bearer ${token}`}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return next.handle(request).pipe(
|
|
||||||
catchError((err) => {
|
|
||||||
if (err instanceof HttpErrorResponse) {
|
|
||||||
if (err.status === 401 && this.authService.isLoggedIn()) {
|
|
||||||
this.snackBar.open('Sitzung abgelaufen, bitte erneut anmelden', undefined, { duration: 5000 });
|
|
||||||
this.authService.logout();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return throwError(err);
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
import { HttpClient } from '@angular/common/http';
|
|
||||||
import { TranslateLoader } from '@ngx-translate/core';
|
|
||||||
import { Observable } from 'rxjs';
|
|
||||||
|
|
||||||
import { environment } from '../../../environments/environment';
|
|
||||||
|
|
||||||
export class CustomTranslateLoader implements TranslateLoader {
|
|
||||||
constructor(private http: HttpClient) {}
|
|
||||||
|
|
||||||
getTranslation(lang: string): Observable<any> {
|
|
||||||
return this.http.get(`${environment.apiUrl}translations/${lang}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
import { HttpClientModule } from '@angular/common/http';
|
|
||||||
import { TestBed } from '@angular/core/testing';
|
|
||||||
|
|
||||||
import { UserService } from './user.service';
|
|
||||||
|
|
||||||
describe('UserService', () => {
|
|
||||||
let service: UserService;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
TestBed.configureTestingModule({
|
|
||||||
imports: [ HttpClientModule ]
|
|
||||||
});
|
|
||||||
service = TestBed.inject(UserService);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should be created', () => {
|
|
||||||
expect(service).toBeTruthy();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
import { HttpClient } from '@angular/common/http';
|
|
||||||
import { Injectable } from '@angular/core';
|
|
||||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
|
||||||
import { AuthService } from 'src/app/modules/auth.service';
|
|
||||||
import { environment } from 'src/environments/environment';
|
|
||||||
|
|
||||||
@Injectable({
|
|
||||||
providedIn: 'root'
|
|
||||||
})
|
|
||||||
export class UserService {
|
|
||||||
|
|
||||||
players: any[] = [];
|
|
||||||
|
|
||||||
constructor(private http: HttpClient, private authService: AuthService, private snackBar: MatSnackBar) { }
|
|
||||||
|
|
||||||
|
|
||||||
loadTeamsOfUser() {
|
|
||||||
this.http.get(`${environment.apiUrl}users/${this.authService.user.id}/teams`).subscribe({
|
|
||||||
next: data => { this.players = data as any[]; },
|
|
||||||
error: () => { this.onUserLoadFailure(); }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
onUserLoadFailure() {
|
|
||||||
this.snackBar.open('Teams konnten nicht geladen werden', undefined, {
|
|
||||||
duration: 5000,
|
|
||||||
panelClass: 'snackbar_error'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
export * from './player';
|
|
||||||
export * from './teamrole';
|
|
||||||
export * from './transaction';
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
import { Team } from '../modules/teams/model/team';
|
|
||||||
import { TeamRole } from './teamrole';
|
|
||||||
|
|
||||||
export interface Player {
|
|
||||||
firstName: string;
|
|
||||||
lastName: string;
|
|
||||||
id: number;
|
|
||||||
balance: number;
|
|
||||||
team: Team;
|
|
||||||
teamRole: TeamRole;
|
|
||||||
active: boolean;
|
|
||||||
|
|
||||||
hide: boolean;
|
|
||||||
|
|
||||||
usersPlayer?: boolean;
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
export interface TeamRole {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
export interface Transaction {
|
|
||||||
id: number;
|
|
||||||
amount: number;
|
|
||||||
playerName?: string;
|
|
||||||
date: string;
|
|
||||||
type: string;
|
|
||||||
note: string;
|
|
||||||
isTeamWalletTransaction: boolean;
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
export interface User {
|
|
||||||
createdAt: string;
|
|
||||||
deletedAt: string;
|
|
||||||
email: string;
|
|
||||||
firstName: string;
|
|
||||||
lastName: string;
|
|
||||||
id: number;
|
|
||||||
photo: string;
|
|
||||||
role: Role;
|
|
||||||
status: any;
|
|
||||||
updatedAt: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Role {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
}
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
import { HttpClient } from '@angular/common/http';
|
|
||||||
import { TestBed } from '@angular/core/testing';
|
|
||||||
import { of } from 'rxjs';
|
|
||||||
|
|
||||||
import { AuthService } from './auth.service';
|
|
||||||
|
|
||||||
describe('AuthService', () => {
|
|
||||||
let service: AuthService;
|
|
||||||
const mockHttp = jasmine.createSpyObj('HttpClient', ['post']);
|
|
||||||
mockHttp.post.and.returnValue(of({
|
|
||||||
token: 'token',
|
|
||||||
user: {id: 1, email: 'mail', firstName: 'first', lastName: 'last'}
|
|
||||||
}))
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
TestBed.configureTestingModule({
|
|
||||||
imports: [],
|
|
||||||
providers: [{ provide: HttpClient, useValue: mockHttp }]
|
|
||||||
});
|
|
||||||
service = TestBed.inject(AuthService);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should be created', () => {
|
|
||||||
expect(service).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should log in', async () => {
|
|
||||||
expect(service['authInfoSubject'].value).toBeNull();
|
|
||||||
const success = await service.login('mail', 'passwort123');
|
|
||||||
expect(success).toBeTrue();
|
|
||||||
expect(service['authInfoSubject'].value).not.toBeNull();
|
|
||||||
expect(service['authInfoSubject'].value.id).toBe(1);
|
|
||||||
expect(service['authInfoSubject'].value.email).toEqual('mail');
|
|
||||||
expect(service['authInfoSubject'].value.firstName).toEqual('first');
|
|
||||||
expect(service['authInfoSubject'].value.lastName).toEqual('last');
|
|
||||||
})
|
|
||||||
});
|
|
||||||
@@ -1,117 +0,0 @@
|
|||||||
import { HttpClient } from '@angular/common/http';
|
|
||||||
import { Injectable } from '@angular/core';
|
|
||||||
import { Router } from '@angular/router';
|
|
||||||
import { BehaviorSubject, Observable } from 'rxjs';
|
|
||||||
import { environment } from 'src/environments/environment';
|
|
||||||
import { User } from '../model/user';
|
|
||||||
|
|
||||||
@Injectable({
|
|
||||||
providedIn: 'root'
|
|
||||||
})
|
|
||||||
export class AuthService {
|
|
||||||
|
|
||||||
private authInfoSubject: BehaviorSubject<User> = new BehaviorSubject(null as any);
|
|
||||||
authInfo$: Observable<User> = this.authInfoSubject.asObservable();
|
|
||||||
|
|
||||||
private token: string = '';
|
|
||||||
|
|
||||||
constructor(private http: HttpClient, private router: Router) {
|
|
||||||
}
|
|
||||||
|
|
||||||
get isAdmin(): boolean {
|
|
||||||
return this.user != null && this.user.role != null && this.user.role.id == 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
isLoggedIn(): boolean {
|
|
||||||
return this.authInfoSubject.value != null;
|
|
||||||
}
|
|
||||||
|
|
||||||
get user(): User {
|
|
||||||
return this.authInfoSubject.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async login(email: string, password: string): Promise<boolean> {
|
|
||||||
return new Promise(resolve => {
|
|
||||||
this.http.post(`${environment.apiUrl}auth/email/login`, { email, password }).subscribe((result: any) => {
|
|
||||||
localStorage.setItem('accessToken', window.btoa(result['token']));
|
|
||||||
this.authInfoSubject.next(result['user']);
|
|
||||||
this.token = result['token'];
|
|
||||||
resolve(true);
|
|
||||||
}, error => {
|
|
||||||
this.http.post(`${environment.apiUrl}auth/admin/email/login`, { email, password }).subscribe((result: any) => {
|
|
||||||
if (result && result['token']) {
|
|
||||||
localStorage.setItem('accessToken', window.btoa(result['token']));
|
|
||||||
this.authInfoSubject.next(result['user']);
|
|
||||||
this.token = result['token'];
|
|
||||||
resolve(true)
|
|
||||||
}
|
|
||||||
}, error => {
|
|
||||||
resolve(false);
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
getAuthToken(): string {
|
|
||||||
return this.token;
|
|
||||||
}
|
|
||||||
|
|
||||||
async verifyAccessToken(token: string): Promise<boolean> {
|
|
||||||
this.token = token;
|
|
||||||
return new Promise(resolve => {
|
|
||||||
this.http.get<User>(`${environment.apiUrl}auth/me`).subscribe(res => {
|
|
||||||
if (res && res.firstName) {
|
|
||||||
this.authInfoSubject.next(res);
|
|
||||||
if ((res as any)['token']) {
|
|
||||||
const token = (res as any)['token'];
|
|
||||||
this.token = token;
|
|
||||||
localStorage.setItem('accessToken', window.btoa(token));
|
|
||||||
}
|
|
||||||
return resolve(true);
|
|
||||||
}
|
|
||||||
return resolve(false);
|
|
||||||
|
|
||||||
}, () => {
|
|
||||||
return resolve(false);
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
logout() {
|
|
||||||
this.authInfoSubject.next(null as any);
|
|
||||||
localStorage.removeItem('accessToken');
|
|
||||||
this.router.navigate(['auth'])
|
|
||||||
}
|
|
||||||
|
|
||||||
register(data: {email: string, password: string, firstName: string, lastName: string, linkPlayerId?: number}): Observable<any> {
|
|
||||||
return this.http.post(`${environment.apiUrl}auth/email/register`, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
forgotPassword(email: string): Observable<any> {
|
|
||||||
return this.http.post(`${environment.apiUrl}auth/forgot/password`, { email });
|
|
||||||
}
|
|
||||||
|
|
||||||
resetPassword(hash: string, password: string): Observable<any> {
|
|
||||||
return this.http.post(`${environment.apiUrl}auth/reset/password`, { hash, password });
|
|
||||||
}
|
|
||||||
|
|
||||||
async loginFromLocalStorage(suppressNavigation = false): Promise<boolean> {
|
|
||||||
return new Promise<boolean>(async resolve => {
|
|
||||||
const authToken = localStorage.getItem('accessToken');
|
|
||||||
|
|
||||||
if (!authToken || authToken.length == 0) {
|
|
||||||
if (!suppressNavigation) {
|
|
||||||
this.router.navigate(['/auth']).then();
|
|
||||||
}
|
|
||||||
return resolve(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
const accessToken = window.atob(authToken);
|
|
||||||
|
|
||||||
|
|
||||||
const success = await this.verifyAccessToken(accessToken);
|
|
||||||
resolve(success)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
<mat-card>
|
|
||||||
<router-outlet></router-outlet>
|
|
||||||
</mat-card>
|
|
||||||
|
|
||||||
<div class="cached_teams" *ngIf="cachedTeams.length > 0" >
|
|
||||||
<div>Teams:</div>
|
|
||||||
@for(t of cachedTeams; track t) {
|
|
||||||
<button mat-flat-button (click)="openTeam(t.alias)" >{{ t.name }}</button>
|
|
||||||
}
|
|
||||||
|
|
||||||
<button mat-stroked-button color="warn" style="margin-top: 16px;" (click)="clear()" >Clear</button>
|
|
||||||
</div>
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
:host {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
width: 100vw;
|
|
||||||
height: 100%;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 64px;
|
|
||||||
}
|
|
||||||
|
|
||||||
mat-card {
|
|
||||||
width: 500px;
|
|
||||||
max-width: 90vw;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cached_teams {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: stretch;
|
|
||||||
gap: 4px;
|
|
||||||
|
|
||||||
button {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
|
||||||
import { MatCardModule } from '@angular/material/card';
|
|
||||||
import { RouterModule } from '@angular/router';
|
|
||||||
|
|
||||||
import { AuthComponent } from './auth.component';
|
|
||||||
|
|
||||||
describe('AuthComponent', () => {
|
|
||||||
let component: AuthComponent;
|
|
||||||
let fixture: ComponentFixture<AuthComponent>;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
await TestBed.configureTestingModule({
|
|
||||||
declarations: [ AuthComponent ],
|
|
||||||
imports: [ MatCardModule, RouterModule ],
|
|
||||||
providers: [ ]
|
|
||||||
})
|
|
||||||
.compileComponents();
|
|
||||||
|
|
||||||
fixture = TestBed.createComponent(AuthComponent);
|
|
||||||
component = fixture.componentInstance;
|
|
||||||
fixture.detectChanges();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should create', () => {
|
|
||||||
expect(component).toBeTruthy();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
import { Component, inject } from '@angular/core';
|
|
||||||
import { AuthService } from '../auth.service';
|
|
||||||
import { Router, RouterModule } from '@angular/router';
|
|
||||||
import { CommonModule } from '@angular/common';
|
|
||||||
import { HttpClientModule } from '@angular/common/http';
|
|
||||||
import { MatCardModule } from '@angular/material/card';
|
|
||||||
import { AuthGuard } from '../../core/guards/auth.guard';
|
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-auth',
|
|
||||||
templateUrl: './auth.component.html',
|
|
||||||
styleUrls: ['./auth.component.scss'],
|
|
||||||
standalone: true,
|
|
||||||
imports: [
|
|
||||||
CommonModule,
|
|
||||||
MatCardModule,
|
|
||||||
HttpClientModule,
|
|
||||||
RouterModule,
|
|
||||||
MatButtonModule
|
|
||||||
],
|
|
||||||
providers: [ AuthGuard ]
|
|
||||||
})
|
|
||||||
export class AuthComponent {
|
|
||||||
|
|
||||||
private auth: AuthService = inject(AuthService);
|
|
||||||
private router: Router = inject(Router);
|
|
||||||
|
|
||||||
cachedTeams: any[] = [];
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
// this.login();
|
|
||||||
this.getCachedTeams();
|
|
||||||
}
|
|
||||||
|
|
||||||
login() {
|
|
||||||
this.auth.loginFromLocalStorage().then(res => {
|
|
||||||
if (res) {
|
|
||||||
this.router.navigate(['/dashboard']);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
getCachedTeams() {
|
|
||||||
let t = localStorage.getItem('cached_teams');
|
|
||||||
if (!t) { return; }
|
|
||||||
|
|
||||||
let teams = JSON.parse(t);
|
|
||||||
this.cachedTeams = Object.keys(teams).map((k: any) => {
|
|
||||||
return { alias: k, name: teams[k] }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
openTeam(alias: string) {
|
|
||||||
this.router.navigate([`teams/${alias}`])
|
|
||||||
}
|
|
||||||
|
|
||||||
clear() {
|
|
||||||
localStorage.removeItem('cached_teams');
|
|
||||||
this.cachedTeams = [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import { Routes } from '@angular/router';
|
|
||||||
import { AuthComponent } from './auth.component';
|
|
||||||
|
|
||||||
export const authRoutes: Routes = [
|
|
||||||
{
|
|
||||||
path: '', component: AuthComponent,
|
|
||||||
children: [{
|
|
||||||
path: '', loadChildren: () => import('./login/login.module').then(m => m.LoginModule)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'register', loadChildren: () => import('./register/register.module').then(m => m.RegisterModule)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'forgot-password', loadComponent: () => import('./forgot-password/forgot-password.component').then(m => m.ForgotPasswordComponent)
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
|
|
||||||
];
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
<mat-card-header>
|
|
||||||
<mat-card-title>Passwort vergessen</mat-card-title>
|
|
||||||
</mat-card-header>
|
|
||||||
|
|
||||||
<mat-card-content>
|
|
||||||
|
|
||||||
<ng-container *ngIf="!submitted">
|
|
||||||
<p class="mat-body">Gib deine E-Mail-Adresse ein. Falls sie bei uns registriert ist, schicken wir dir einen Link zum Zurücksetzen des Passworts.</p>
|
|
||||||
|
|
||||||
<form [formGroup]="form" (keyup.enter)="submit()">
|
|
||||||
<mat-form-field appearance="outline">
|
|
||||||
<mat-label>Email</mat-label>
|
|
||||||
<input matInput placeholder="pat@example.com" formControlName="email" required>
|
|
||||||
</mat-form-field>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<p *ngIf="error" class="forgot-error">Anfrage konnte nicht gesendet werden. Bitte später erneut versuchen.</p>
|
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
<ng-container *ngIf="submitted">
|
|
||||||
<p class="mat-body">Falls die E-Mail-Adresse bei uns registriert ist, wurde eine Nachricht mit einem Link zum Zurücksetzen des Passworts verschickt. Bitte Posteingang (und Spam-Ordner) prüfen.</p>
|
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
</mat-card-content>
|
|
||||||
|
|
||||||
<mat-card-actions align="end">
|
|
||||||
<a mat-button routerLink="/auth">Zurück zum Login</a>
|
|
||||||
<button *ngIf="!submitted" mat-button color="primary" [disabled]="form.invalid" (click)="submit()">Link anfordern</button>
|
|
||||||
</mat-card-actions>
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
mat-form-field {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
mat-card-header {
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.forgot-error {
|
|
||||||
color: #b3261e;
|
|
||||||
margin: 8px 0 0;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
|
||||||
import { Component } from '@angular/core';
|
|
||||||
import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
|
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
|
||||||
import { MatCardModule } from '@angular/material/card';
|
|
||||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
|
||||||
import { MatInputModule } from '@angular/material/input';
|
|
||||||
import { RouterModule } from '@angular/router';
|
|
||||||
import { AuthService } from '../../auth.service';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-forgot-password',
|
|
||||||
templateUrl: './forgot-password.component.html',
|
|
||||||
styleUrls: ['./forgot-password.component.scss'],
|
|
||||||
standalone: true,
|
|
||||||
imports: [
|
|
||||||
CommonModule,
|
|
||||||
ReactiveFormsModule,
|
|
||||||
MatCardModule,
|
|
||||||
MatFormFieldModule,
|
|
||||||
MatInputModule,
|
|
||||||
MatButtonModule,
|
|
||||||
RouterModule
|
|
||||||
]
|
|
||||||
})
|
|
||||||
export class ForgotPasswordComponent {
|
|
||||||
|
|
||||||
submitted = false;
|
|
||||||
error = false;
|
|
||||||
|
|
||||||
form = new FormGroup({
|
|
||||||
email: new FormControl('', [Validators.required, Validators.email])
|
|
||||||
});
|
|
||||||
|
|
||||||
constructor(private authService: AuthService) {}
|
|
||||||
|
|
||||||
submit() {
|
|
||||||
if (this.form.invalid) { return; }
|
|
||||||
const email = this.form.controls.email.value + '';
|
|
||||||
this.error = false;
|
|
||||||
|
|
||||||
this.authService.forgotPassword(email).subscribe({
|
|
||||||
next: () => { this.submitted = true; },
|
|
||||||
error: (err) => {
|
|
||||||
// Aus Datenschutzgründen wird bei unbekannter E-Mail dieselbe
|
|
||||||
// Erfolgsmeldung angezeigt wie bei einer bekannten Adresse.
|
|
||||||
if (err?.error?.errors?.email === 'emailNotExists') {
|
|
||||||
this.submitted = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.error = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
|
|
||||||
import { NgModule } from '@angular/core';
|
|
||||||
import { RouterModule, Routes } from '@angular/router';
|
|
||||||
import { LoginComponent } from './login.component';
|
|
||||||
|
|
||||||
const routes: Routes = [
|
|
||||||
{
|
|
||||||
path: '', component: LoginComponent
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
@NgModule({
|
|
||||||
imports: [RouterModule.forChild(routes)],
|
|
||||||
exports: [RouterModule]
|
|
||||||
})
|
|
||||||
export class LoginRoutingModule { }
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
<mat-card-header>
|
|
||||||
<mat-card-title>Login</mat-card-title>
|
|
||||||
</mat-card-header>
|
|
||||||
|
|
||||||
<mat-card-content>
|
|
||||||
<form [formGroup]="loginForm" (keyup.enter)="login()">
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<mat-form-field appearance="outline">
|
|
||||||
<mat-label>Email</mat-label>
|
|
||||||
<input matInput placeholder="pat@example.com" formControlName="email" required name="username">
|
|
||||||
</mat-form-field>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<mat-form-field appearance="outline">
|
|
||||||
<mat-label>Passwort</mat-label>
|
|
||||||
<input matInput autocomplete="current-password" type="password" placeholder="pat@example.com" formControlName="password" required>
|
|
||||||
</mat-form-field>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p *ngIf="loginFailed" class="login-error">E-Mail oder Passwort ist falsch.</p>
|
|
||||||
|
|
||||||
</form>
|
|
||||||
|
|
||||||
</mat-card-content>
|
|
||||||
|
|
||||||
<mat-card-actions align="end">
|
|
||||||
<a mat-button routerLink="/auth/forgot-password">Passwort vergessen?</a>
|
|
||||||
<button mat-button color="primary" [disabled]="loginForm.invalid" (click)="login()">Login</button>
|
|
||||||
</mat-card-actions>
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
mat-form-field {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
mat-card-header {
|
|
||||||
margin-bottom: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-error {
|
|
||||||
color: #b3261e;
|
|
||||||
margin: 0 0 8px;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
|
||||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
|
||||||
import { MatCardModule } from '@angular/material/card';
|
|
||||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
|
||||||
import { MatInputModule } from '@angular/material/input';
|
|
||||||
import { MatSnackBarModule } from '@angular/material/snack-bar';
|
|
||||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
|
||||||
import { AuthService } from '../../auth.service';
|
|
||||||
|
|
||||||
import { LoginComponent } from './login.component';
|
|
||||||
|
|
||||||
describe('LoginComponent', () => {
|
|
||||||
let component: LoginComponent;
|
|
||||||
let fixture: ComponentFixture<LoginComponent>;
|
|
||||||
const mockAuthService = jasmine.createSpyObj('AuthService', [''])
|
|
||||||
beforeEach(async () => {
|
|
||||||
await TestBed.configureTestingModule({
|
|
||||||
declarations: [ LoginComponent ],
|
|
||||||
providers: [
|
|
||||||
{ provide: AuthService, useValue: mockAuthService }
|
|
||||||
],
|
|
||||||
imports: [ MatCardModule, FormsModule, MatFormFieldModule, ReactiveFormsModule, MatInputModule, MatButtonModule, MatSnackBarModule, NoopAnimationsModule ]
|
|
||||||
})
|
|
||||||
.compileComponents();
|
|
||||||
|
|
||||||
fixture = TestBed.createComponent(LoginComponent);
|
|
||||||
component = fixture.componentInstance;
|
|
||||||
fixture.detectChanges();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should create', () => {
|
|
||||||
expect(component).toBeTruthy();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
import { Component } from '@angular/core';
|
|
||||||
import { FormGroup, FormControl, Validators } from '@angular/forms';
|
|
||||||
import { Router } from '@angular/router';
|
|
||||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
|
||||||
import { AuthService } from '../../auth.service';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-login',
|
|
||||||
templateUrl: './login.component.html',
|
|
||||||
styleUrls: ['./login.component.scss']
|
|
||||||
})
|
|
||||||
export class LoginComponent {
|
|
||||||
|
|
||||||
loginFailed = false;
|
|
||||||
|
|
||||||
constructor(private authService: AuthService, private router: Router, private snackBar: MatSnackBar) {
|
|
||||||
}
|
|
||||||
|
|
||||||
loginForm = new FormGroup({
|
|
||||||
email: new FormControl('', [Validators.required, Validators.email]),
|
|
||||||
password: new FormControl('', [Validators.required])
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
async login() {
|
|
||||||
const email = this.loginForm.controls.email.value + '';
|
|
||||||
const password = this.loginForm.controls.password.value + ''
|
|
||||||
this.loginFailed = false;
|
|
||||||
const success = await this.authService.login(email, password);
|
|
||||||
|
|
||||||
if (success) {
|
|
||||||
this.router.navigate(['/dashboard']).then();
|
|
||||||
} else {
|
|
||||||
this.loginFailed = true;
|
|
||||||
this.snackBar.open('E-Mail oder Passwort ist falsch', undefined, {
|
|
||||||
duration: 5000,
|
|
||||||
panelClass: 'snackbar_error'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
import { NgModule } from '@angular/core';
|
|
||||||
import { CommonModule } from '@angular/common';
|
|
||||||
import { LoginComponent } from './login.component';
|
|
||||||
import { LoginRoutingModule } from './login-routing.module';
|
|
||||||
import { MatCardModule } from '@angular/material/card';
|
|
||||||
import {MatButtonModule} from '@angular/material/button';
|
|
||||||
import {MatFormFieldModule} from '@angular/material/form-field';
|
|
||||||
import { ReactiveFormsModule } from '@angular/forms';
|
|
||||||
import {MatInputModule} from '@angular/material/input';
|
|
||||||
|
|
||||||
@NgModule({
|
|
||||||
declarations: [
|
|
||||||
LoginComponent
|
|
||||||
],
|
|
||||||
imports: [
|
|
||||||
CommonModule,
|
|
||||||
LoginRoutingModule,
|
|
||||||
MatCardModule,
|
|
||||||
MatButtonModule,
|
|
||||||
MatFormFieldModule,
|
|
||||||
MatInputModule,
|
|
||||||
ReactiveFormsModule
|
|
||||||
]
|
|
||||||
})
|
|
||||||
export class LoginModule { }
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
<mat-card>
|
|
||||||
<mat-card-header>
|
|
||||||
<mat-card-title>Neues Passwort vergeben</mat-card-title>
|
|
||||||
</mat-card-header>
|
|
||||||
|
|
||||||
<mat-card-content>
|
|
||||||
|
|
||||||
<ng-container *ngIf="!success && hash !== null">
|
|
||||||
<form [formGroup]="form" (keyup.enter)="submit()">
|
|
||||||
<mat-form-field appearance="outline">
|
|
||||||
<mat-label>Neues Passwort</mat-label>
|
|
||||||
<input matInput type="password" autocomplete="new-password" formControlName="password" required>
|
|
||||||
<mat-hint>Mindestens 6 Zeichen</mat-hint>
|
|
||||||
</mat-form-field>
|
|
||||||
|
|
||||||
<mat-form-field appearance="outline">
|
|
||||||
<mat-label>Passwort wiederholen</mat-label>
|
|
||||||
<input matInput type="password" autocomplete="new-password" formControlName="confirmPassword" required>
|
|
||||||
</mat-form-field>
|
|
||||||
|
|
||||||
<p *ngIf="form.errors?.['passwordMismatch'] && form.controls.confirmPassword.dirty" class="password-error">
|
|
||||||
Die Passwörter stimmen nicht überein.
|
|
||||||
</p>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<p *ngIf="error" class="password-error">
|
|
||||||
Der Link ist ungültig oder abgelaufen. Bitte fordere über "Passwort vergessen" einen neuen Link an.
|
|
||||||
</p>
|
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
<ng-container *ngIf="success">
|
|
||||||
<p class="mat-body">Dein Passwort wurde geändert. Du wirst gleich zum Login weitergeleitet.</p>
|
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
<ng-container *ngIf="hash === null">
|
|
||||||
<p class="mat-body">Dieser Link ist unvollständig. Bitte fordere über "Passwort vergessen" einen neuen Link an.</p>
|
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
</mat-card-content>
|
|
||||||
|
|
||||||
<mat-card-actions align="end">
|
|
||||||
<a mat-button routerLink="/auth">Zurück zum Login</a>
|
|
||||||
<button *ngIf="!success && hash !== null" mat-button color="primary" [disabled]="form.invalid" (click)="submit()">Passwort speichern</button>
|
|
||||||
</mat-card-actions>
|
|
||||||
</mat-card>
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
:host {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
width: 100vw;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
mat-card {
|
|
||||||
width: 500px;
|
|
||||||
max-width: 90vw;
|
|
||||||
}
|
|
||||||
|
|
||||||
mat-form-field {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
mat-card-header {
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.password-error {
|
|
||||||
color: #b3261e;
|
|
||||||
margin: 8px 0 0;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
|
||||||
import { Component, OnInit } from '@angular/core';
|
|
||||||
import { AbstractControl, FormControl, FormGroup, ReactiveFormsModule, ValidationErrors, Validators } from '@angular/forms';
|
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
|
||||||
import { MatCardModule } from '@angular/material/card';
|
|
||||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
|
||||||
import { MatInputModule } from '@angular/material/input';
|
|
||||||
import { ActivatedRoute, Router, RouterModule } from '@angular/router';
|
|
||||||
import { AuthService } from '../../auth.service';
|
|
||||||
|
|
||||||
function passwordsMatch(control: AbstractControl): ValidationErrors | null {
|
|
||||||
const password = control.get('password')?.value;
|
|
||||||
const confirmPassword = control.get('confirmPassword')?.value;
|
|
||||||
return password && confirmPassword && password !== confirmPassword ? { passwordMismatch: true } : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-password-change',
|
|
||||||
templateUrl: './password-change.component.html',
|
|
||||||
styleUrls: ['./password-change.component.scss'],
|
|
||||||
standalone: true,
|
|
||||||
imports: [
|
|
||||||
CommonModule,
|
|
||||||
ReactiveFormsModule,
|
|
||||||
MatCardModule,
|
|
||||||
MatFormFieldModule,
|
|
||||||
MatInputModule,
|
|
||||||
MatButtonModule,
|
|
||||||
RouterModule
|
|
||||||
]
|
|
||||||
})
|
|
||||||
export class PasswordChangeComponent implements OnInit {
|
|
||||||
|
|
||||||
hash: string | null = null;
|
|
||||||
success = false;
|
|
||||||
error = false;
|
|
||||||
|
|
||||||
form = new FormGroup({
|
|
||||||
password: new FormControl('', [Validators.required, Validators.minLength(6)]),
|
|
||||||
confirmPassword: new FormControl('', [Validators.required]),
|
|
||||||
}, { validators: passwordsMatch });
|
|
||||||
|
|
||||||
constructor(private route: ActivatedRoute, private router: Router, private authService: AuthService) {}
|
|
||||||
|
|
||||||
ngOnInit(): void {
|
|
||||||
this.hash = this.route.snapshot.paramMap.get('hash');
|
|
||||||
if (!this.hash) {
|
|
||||||
this.error = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
submit() {
|
|
||||||
if (this.form.invalid || !this.hash) { return; }
|
|
||||||
this.error = false;
|
|
||||||
|
|
||||||
this.authService.resetPassword(this.hash, this.form.controls.password.value + '').subscribe({
|
|
||||||
next: () => {
|
|
||||||
this.success = true;
|
|
||||||
setTimeout(() => this.router.navigateByUrl('/auth'), 3000);
|
|
||||||
},
|
|
||||||
error: () => { this.error = true; }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
<mat-card-header>
|
|
||||||
<mat-card-title>Registrieren nicht möglich</mat-card-title>
|
|
||||||
</mat-card-header>
|
|
||||||
|
|
||||||
<mat-card-content>
|
|
||||||
<div class="mat-body">
|
|
||||||
Es ist kein gültiger Registrierungstoken vorhanden. Das Registrieren ist derzeit nur über einen Link mit gültigem Token möglich.
|
|
||||||
</div>
|
|
||||||
</mat-card-content>
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
|
||||||
|
|
||||||
import { NoTokenProvidedComponent } from './no-token-provided.component';
|
|
||||||
|
|
||||||
describe('NoTokenProvidedComponent', () => {
|
|
||||||
let component: NoTokenProvidedComponent;
|
|
||||||
let fixture: ComponentFixture<NoTokenProvidedComponent>;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
await TestBed.configureTestingModule({
|
|
||||||
declarations: [ NoTokenProvidedComponent ]
|
|
||||||
})
|
|
||||||
.compileComponents();
|
|
||||||
|
|
||||||
fixture = TestBed.createComponent(NoTokenProvidedComponent);
|
|
||||||
component = fixture.componentInstance;
|
|
||||||
fixture.detectChanges();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should create', () => {
|
|
||||||
expect(component).toBeTruthy();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
import { Component } from '@angular/core';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-no-token-provided',
|
|
||||||
templateUrl: './no-token-provided.component.html',
|
|
||||||
styleUrls: ['./no-token-provided.component.scss']
|
|
||||||
})
|
|
||||||
export class NoTokenProvidedComponent {
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
<router-outlet></router-outlet>
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
|
||||||
|
|
||||||
import { RegisterBaseComponent } from './register-base.component';
|
|
||||||
|
|
||||||
describe('RegisterBaseComponent', () => {
|
|
||||||
let component: RegisterBaseComponent;
|
|
||||||
let fixture: ComponentFixture<RegisterBaseComponent>;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
await TestBed.configureTestingModule({
|
|
||||||
declarations: [ RegisterBaseComponent ]
|
|
||||||
})
|
|
||||||
.compileComponents();
|
|
||||||
|
|
||||||
fixture = TestBed.createComponent(RegisterBaseComponent);
|
|
||||||
component = fixture.componentInstance;
|
|
||||||
fixture.detectChanges();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should create', () => {
|
|
||||||
expect(component).toBeTruthy();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
import { Component } from '@angular/core';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-register-base',
|
|
||||||
templateUrl: './register-base.component.html',
|
|
||||||
styleUrls: ['./register-base.component.scss']
|
|
||||||
})
|
|
||||||
export class RegisterBaseComponent {
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
<form [formGroup]="registerForm" (keyup.enter)="register()">
|
|
||||||
|
|
||||||
<div class="splitted_form">
|
|
||||||
<mat-form-field appearance="outline" class="first">
|
|
||||||
<mat-label>Vorname</mat-label>
|
|
||||||
<input matInput autocomplete="current-password" type="text" placeholder="Max" formControlName="firstName" required>
|
|
||||||
</mat-form-field>
|
|
||||||
|
|
||||||
<mat-form-field appearance="outline" class="last">
|
|
||||||
<mat-label>Nachname</mat-label>
|
|
||||||
<input matInput autocomplete="current-password" type="text" placeholder="Mustermann" formControlName="lastName" required>
|
|
||||||
</mat-form-field>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<mat-form-field appearance="outline">
|
|
||||||
<mat-label>Email</mat-label>
|
|
||||||
<input matInput placeholder="pat@example.com" formControlName="email" required>
|
|
||||||
</mat-form-field>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<mat-form-field appearance="outline">
|
|
||||||
<mat-label>Passwort</mat-label>
|
|
||||||
<input matInput autocomplete="current-password" type="password" formControlName="password" required>
|
|
||||||
<mat-hint>Mindestens 6 Zeichen</mat-hint>
|
|
||||||
</mat-form-field>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</form>
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
mat-form-field {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
|
||||||
|
|
||||||
import { RegisterFormComponent } from './register-form.component';
|
|
||||||
|
|
||||||
describe('RegisterFormComponent', () => {
|
|
||||||
let component: RegisterFormComponent;
|
|
||||||
let fixture: ComponentFixture<RegisterFormComponent>;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
await TestBed.configureTestingModule({
|
|
||||||
declarations: [ RegisterFormComponent ]
|
|
||||||
})
|
|
||||||
.compileComponents();
|
|
||||||
|
|
||||||
fixture = TestBed.createComponent(RegisterFormComponent);
|
|
||||||
component = fixture.componentInstance;
|
|
||||||
fixture.detectChanges();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should create', () => {
|
|
||||||
expect(component).toBeTruthy();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
import { Component, Input, OnInit } from '@angular/core';
|
|
||||||
import { FormGroup, FormControl, Validators } from '@angular/forms';
|
|
||||||
import { TeamInvite } from 'src/app/modules/teams/model/team-invite';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-register-form',
|
|
||||||
templateUrl: './register-form.component.html',
|
|
||||||
styleUrls: ['./register-form.component.scss']
|
|
||||||
})
|
|
||||||
export class RegisterFormComponent implements OnInit {
|
|
||||||
|
|
||||||
|
|
||||||
@Input('teamInfo') teamInfo: TeamInvite | undefined;
|
|
||||||
|
|
||||||
registerForm = new FormGroup({
|
|
||||||
email: new FormControl('', [Validators.required, Validators.email]),
|
|
||||||
password: new FormControl('', [Validators.required, Validators.minLength(6)]),
|
|
||||||
lastName: new FormControl('', [Validators.required]),
|
|
||||||
firstName: new FormControl('', [Validators.required]),
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
ngOnInit(): void {
|
|
||||||
if (this.teamInfo) {
|
|
||||||
let names = this.teamInfo.playerName.split(' ');
|
|
||||||
if (names && names.length == 2) {
|
|
||||||
this.registerForm.controls.firstName.patchValue(names[0])
|
|
||||||
this.registerForm.controls.lastName.patchValue(names[1])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
register() {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
<mat-card-header>
|
|
||||||
<mat-card-title>Registrieren</mat-card-title>
|
|
||||||
</mat-card-header>
|
|
||||||
|
|
||||||
<mat-card-content>
|
|
||||||
<ng-container *ngIf="teamInfo">
|
|
||||||
<div class="mat-body">Hallo <span class="info">{{ teamInfo.playerName }}</span>,</div>
|
|
||||||
<span>
|
|
||||||
|
|
||||||
du wurdes eingeladen dich für das Team <span class="info">{{ teamInfo.teamName }}</span> zu registrieren.
|
|
||||||
|
|
||||||
</span>
|
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
<div *ngIf="teamInfo" class="register_form">
|
|
||||||
<app-register-form [teamInfo]="teamInfo" #formComponent></app-register-form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="error" *ngIf="error">
|
|
||||||
Registrierung fehlgeschlagen. Bitte wende dich an deinen Ansprechpartner.
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</mat-card-content>
|
|
||||||
|
|
||||||
<mat-card-actions align="end">
|
|
||||||
<button mat-button color="primary" [disabled]="registerFormInvalid" (click)="register()">Registrieren</button>
|
|
||||||
</mat-card-actions>
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
.info {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.register_form {
|
|
||||||
margin-top: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.error {
|
|
||||||
margin-top: 24px;
|
|
||||||
color: red;
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
|
||||||
|
|
||||||
import { RegisterComponent } from './register.component';
|
|
||||||
|
|
||||||
describe('RegisterComponent', () => {
|
|
||||||
let component: RegisterComponent;
|
|
||||||
let fixture: ComponentFixture<RegisterComponent>;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
await TestBed.configureTestingModule({
|
|
||||||
declarations: [ RegisterComponent ]
|
|
||||||
})
|
|
||||||
.compileComponents();
|
|
||||||
|
|
||||||
fixture = TestBed.createComponent(RegisterComponent);
|
|
||||||
component = fixture.componentInstance;
|
|
||||||
fixture.detectChanges();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should create', () => {
|
|
||||||
expect(component).toBeTruthy();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
import { Component, OnInit, ViewChild } from '@angular/core';
|
|
||||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
|
||||||
import { AuthService } from 'src/app/modules/auth.service';
|
|
||||||
import { TeamsService } from 'src/app/modules/teams/teams.service';
|
|
||||||
import { RegisterFormComponent } from './register-form/register-form.component';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-register',
|
|
||||||
templateUrl: './register.component.html',
|
|
||||||
styleUrls: ['./register.component.scss']
|
|
||||||
})
|
|
||||||
export class RegisterComponent implements OnInit {
|
|
||||||
|
|
||||||
private token: string | null= null;
|
|
||||||
public teamInfo: { playerId: number, playerName: string, teamId: number, teamName: string} | undefined;
|
|
||||||
error: boolean = false;
|
|
||||||
|
|
||||||
constructor(private route: ActivatedRoute, private router: Router, private teamsService: TeamsService, private authService: AuthService, private snackBar: MatSnackBar) {}
|
|
||||||
|
|
||||||
|
|
||||||
@ViewChild('formComponent') formComponent!: RegisterFormComponent;
|
|
||||||
|
|
||||||
|
|
||||||
ngOnInit(): void {
|
|
||||||
this.validateToken(this.route.snapshot.paramMap.get('token'));
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private async validateToken(token: string | null) {
|
|
||||||
|
|
||||||
if (!token) {
|
|
||||||
return this.onNoTokenProvided();
|
|
||||||
} else {
|
|
||||||
this.teamsService.validateRegistrationToken(token).subscribe({
|
|
||||||
next: data => { this.teamInfo = data; },
|
|
||||||
error: () => { return this.onNoTokenProvided(); }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
this.token = token;
|
|
||||||
}
|
|
||||||
|
|
||||||
private onNoTokenProvided() {
|
|
||||||
this.router.navigateByUrl('auth/register/no-token');
|
|
||||||
}
|
|
||||||
|
|
||||||
get registerForm() {
|
|
||||||
return this.formComponent?.registerForm;
|
|
||||||
}
|
|
||||||
|
|
||||||
get registerFormInvalid(): boolean {
|
|
||||||
return !this.registerForm || this.registerForm.invalid;
|
|
||||||
}
|
|
||||||
|
|
||||||
register() {
|
|
||||||
const data = this.registerForm.value as any;
|
|
||||||
data.linkPlayerId = this.teamInfo?.playerId;
|
|
||||||
|
|
||||||
this.authService.register(data).subscribe({
|
|
||||||
next: () => { this.onPlayerRegistrationSuccess() },
|
|
||||||
error: () => { this.onPlayerRegistrationFailure() }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
onPlayerRegistrationSuccess() {
|
|
||||||
this.error = false;
|
|
||||||
this.snackBar.open('Erfolgreich registriert', undefined, {
|
|
||||||
duration: 5000
|
|
||||||
});
|
|
||||||
|
|
||||||
this.router.navigateByUrl('/auth')
|
|
||||||
}
|
|
||||||
|
|
||||||
onPlayerRegistrationFailure() {
|
|
||||||
|
|
||||||
this.snackBar.open('Registrierung nicht möglich', undefined, {
|
|
||||||
duration: 5000,
|
|
||||||
panelClass: 'snackbar_error'
|
|
||||||
});
|
|
||||||
|
|
||||||
this.registerForm.reset()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
import { NgModule } from '@angular/core';
|
|
||||||
import { RouterModule, Routes } from '@angular/router';
|
|
||||||
import { NoTokenProvidedComponent } from './components/no-token-provided/no-token-provided.component';
|
|
||||||
import { RegisterBaseComponent } from './components/register-base/register-base.component';
|
|
||||||
import { RegisterComponent } from './components/register/register.component';
|
|
||||||
|
|
||||||
const routes: Routes = [
|
|
||||||
{ path: '', component: RegisterBaseComponent, children: [
|
|
||||||
{
|
|
||||||
path: 'no-token',
|
|
||||||
component: NoTokenProvidedComponent
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: ':token',
|
|
||||||
component: RegisterComponent
|
|
||||||
}
|
|
||||||
] }
|
|
||||||
];
|
|
||||||
|
|
||||||
@NgModule({
|
|
||||||
imports: [RouterModule.forChild(routes)],
|
|
||||||
exports: [RouterModule]
|
|
||||||
})
|
|
||||||
export class RegisterRoutingModule { }
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
import { NgModule } from '@angular/core';
|
|
||||||
import { CommonModule } from '@angular/common';
|
|
||||||
|
|
||||||
import { RegisterRoutingModule } from './register-routing.module';
|
|
||||||
import { NoTokenProvidedComponent } from './components/no-token-provided/no-token-provided.component';
|
|
||||||
import { RegisterBaseComponent } from './components/register-base/register-base.component';
|
|
||||||
import { MatCardModule } from '@angular/material/card';
|
|
||||||
import { RegisterComponent } from './components/register/register.component';
|
|
||||||
import { RegisterFormComponent } from './components/register/register-form/register-form.component';
|
|
||||||
import { ReactiveFormsModule } from '@angular/forms';
|
|
||||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
|
||||||
import { MatInputModule } from '@angular/material/input';
|
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
|
||||||
import { MatSnackBarModule } from '@angular/material/snack-bar';
|
|
||||||
|
|
||||||
|
|
||||||
@NgModule({
|
|
||||||
declarations: [
|
|
||||||
NoTokenProvidedComponent,
|
|
||||||
RegisterBaseComponent,
|
|
||||||
RegisterComponent,
|
|
||||||
RegisterFormComponent
|
|
||||||
],
|
|
||||||
imports: [
|
|
||||||
CommonModule,
|
|
||||||
RegisterRoutingModule,
|
|
||||||
MatCardModule,
|
|
||||||
MatFormFieldModule,
|
|
||||||
MatInputModule,
|
|
||||||
ReactiveFormsModule,
|
|
||||||
MatButtonModule,
|
|
||||||
MatSnackBarModule
|
|
||||||
]
|
|
||||||
})
|
|
||||||
export class RegisterModule { }
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
|
|
||||||
<div class="card mat-elevation-z2">
|
|
||||||
Welcome {{ name }}, your Role is {{ role }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<div class="card mat-elevation-z2">
|
|
||||||
<div class="mat-h2">Deine Teams</div>
|
|
||||||
<div class="mat-body">Klicke auf eins der Teams um es zu verwalten</div>
|
|
||||||
<div *ngFor="let p of players" class="flex-row team_row">
|
|
||||||
<div class="team">
|
|
||||||
<div>
|
|
||||||
<span>{{ p.team.name }}</span>
|
|
||||||
</div>
|
|
||||||
<div>Balance: {{ p.team.balance | currency:'EUR' }}</div>
|
|
||||||
<div>
|
|
||||||
{{ p.firstName }} {{ p.lastName }}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
Rolle: <span>{{ p.teamRole.name | translate }}</span>
|
|
||||||
</div>
|
|
||||||
<button (click)="copyLink(p)" mat-stroked-button >Öffentlichen Link kopieren</button>
|
|
||||||
</div>
|
|
||||||
<div class="icons">
|
|
||||||
<button mat-icon-button (click)="onTeamClick(p.team)" aria-label="Team-Details öffnen">
|
|
||||||
<mat-icon>speed</mat-icon>
|
|
||||||
</button>
|
|
||||||
<button mat-icon-button (click)="onTeamClick(p.team)" aria-label="Team bearbeiten">
|
|
||||||
<mat-icon>edit</mat-icon>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
|
|
||||||
|
|
||||||
.icons {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
opacity: 0;
|
|
||||||
pointer-events: none;
|
|
||||||
transition: opacity 0.2s ease-in-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
.team_row {
|
|
||||||
padding: 2px 8px;
|
|
||||||
border: 1px solid #ccc;
|
|
||||||
border-radius: 4px;
|
|
||||||
align-items: stretch;
|
|
||||||
|
|
||||||
&:hover > .icons {
|
|
||||||
opacity: 1;
|
|
||||||
pointer-events: all;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:not(:first-of-type) {
|
|
||||||
margin-top: 8px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (max-width: 700px) {
|
|
||||||
.icons {
|
|
||||||
opacity: 1;
|
|
||||||
pointer-events: all;
|
|
||||||
align-items: stretch;
|
|
||||||
|
|
||||||
button {
|
|
||||||
height: inherit;
|
|
||||||
width: 48px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
import { HttpClientModule } from '@angular/common/http';
|
|
||||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
|
||||||
|
|
||||||
import { DashboardComponent } from './dashboard.component';
|
|
||||||
|
|
||||||
describe('DashboardComponent', () => {
|
|
||||||
let component: DashboardComponent;
|
|
||||||
let fixture: ComponentFixture<DashboardComponent>;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
await TestBed.configureTestingModule({
|
|
||||||
declarations: [ DashboardComponent ],
|
|
||||||
imports: [ HttpClientModule ]
|
|
||||||
})
|
|
||||||
.compileComponents();
|
|
||||||
|
|
||||||
fixture = TestBed.createComponent(DashboardComponent);
|
|
||||||
component = fixture.componentInstance;
|
|
||||||
fixture.detectChanges();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should create', () => {
|
|
||||||
expect(component).toBeTruthy();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
import { Component } from '@angular/core';
|
|
||||||
import { Router } from '@angular/router';
|
|
||||||
import { TranslateModule, TranslateService } from '@ngx-translate/core';
|
|
||||||
import { UserService } from 'src/app/core/user/user.service';
|
|
||||||
import { AuthService } from '../../auth.service';
|
|
||||||
import { CommonModule } from '@angular/common';
|
|
||||||
import { Player } from 'src/app/model';
|
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
|
||||||
import { MatIconModule } from '@angular/material/icon';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-dashboard',
|
|
||||||
templateUrl: './dashboard.component.html',
|
|
||||||
styleUrls: ['./dashboard.component.scss'],
|
|
||||||
standalone: true,
|
|
||||||
imports: [ CommonModule, TranslateModule, MatButtonModule, MatIconModule ]
|
|
||||||
})
|
|
||||||
export class DashboardComponent {
|
|
||||||
|
|
||||||
link: string = window.location.host;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private authService: AuthService
|
|
||||||
, private userService: UserService
|
|
||||||
, private router: Router
|
|
||||||
, public translate: TranslateService) {}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
get role(): string {
|
|
||||||
|
|
||||||
if (!this.authService || !this.authService.isLoggedIn()) { return ''}
|
|
||||||
|
|
||||||
return this.authService.user.role.name;
|
|
||||||
}
|
|
||||||
|
|
||||||
get name(): string {
|
|
||||||
if (!this.authService || !this.authService.isLoggedIn()) { return ''}
|
|
||||||
|
|
||||||
return this.authService.user.firstName + ' ' + this.authService.user.lastName;
|
|
||||||
}
|
|
||||||
|
|
||||||
get players(): any[] {
|
|
||||||
return this.userService.players;
|
|
||||||
}
|
|
||||||
|
|
||||||
onTeamClick(team: any) {
|
|
||||||
this.router.navigate([`dashboard/${team.alias}/details`])
|
|
||||||
}
|
|
||||||
|
|
||||||
onLinkClick(event: any) {
|
|
||||||
event.stopPropagation();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
async copyLink(p: Player) {
|
|
||||||
const link = `https://${this.link}/teams/${p.team.alias}`
|
|
||||||
await navigator.clipboard.writeText(link);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
import { Routes } from '@angular/router';
|
|
||||||
|
|
||||||
export const dashboardRoutes: Routes = [
|
|
||||||
{ path: '', loadComponent: () => import('./dashboard.component').then(m => m.DashboardComponent) },
|
|
||||||
{ path: ':id/details', loadComponent: () => import('./team-details/team-details.component').then(m => m.TeamDetailsComponent) }
|
|
||||||
];
|
|
||||||
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
<div class="player_icon" [class.line_through]="!player.active" [class.inactive]="!player.active">
|
|
||||||
{{ playerInitials }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="content" [class.inactive]="!player.active">
|
|
||||||
|
|
||||||
<div class="name" [class.line_through]="!player.active">{{ player.firstName}} {{ player.lastName }} </div>
|
|
||||||
<div class="role"> {{ player.teamRole.name | translate }}</div>
|
|
||||||
|
|
||||||
<div class="balance"> {{ 'balance' | translate }}: {{ player.balance | currency:'EUR' }}</div>
|
|
||||||
</div>
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
:host {
|
|
||||||
display: flex;
|
|
||||||
flex: 250px 1 1;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.player_icon {
|
|
||||||
width: 48px;
|
|
||||||
height: 48px;
|
|
||||||
line-height: 48px;
|
|
||||||
text-align: center;
|
|
||||||
font-size: 20px;
|
|
||||||
border-radius: 50%;
|
|
||||||
border: 1px solid #ccc
|
|
||||||
|
|
||||||
}
|
|
||||||
.content{
|
|
||||||
margin-left: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.name {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.line_through{
|
|
||||||
text-decoration: line-through;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inactive {
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
|
||||||
|
|
||||||
import { PlayerCardComponent } from './player-card.component';
|
|
||||||
|
|
||||||
describe('PlayerCardComponent', () => {
|
|
||||||
let component: PlayerCardComponent;
|
|
||||||
let fixture: ComponentFixture<PlayerCardComponent>;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
await TestBed.configureTestingModule({
|
|
||||||
declarations: [ PlayerCardComponent ]
|
|
||||||
})
|
|
||||||
.compileComponents();
|
|
||||||
|
|
||||||
fixture = TestBed.createComponent(PlayerCardComponent);
|
|
||||||
component = fixture.componentInstance;
|
|
||||||
fixture.detectChanges();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should create', () => {
|
|
||||||
expect(component).toBeTruthy();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
|
||||||
import { Component, Input, OnInit, Output } from '@angular/core';
|
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
|
||||||
import { Player } from 'src/app/model';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-player-card',
|
|
||||||
templateUrl: './player-card.component.html',
|
|
||||||
styleUrls: ['./player-card.component.scss'],
|
|
||||||
standalone: true,
|
|
||||||
imports: [CommonModule, TranslateModule]
|
|
||||||
})
|
|
||||||
export class PlayerCardComponent implements OnInit {
|
|
||||||
|
|
||||||
@Input() player!: Player;
|
|
||||||
|
|
||||||
|
|
||||||
ngOnInit(): void {}
|
|
||||||
|
|
||||||
|
|
||||||
get playerInitials(): string {
|
|
||||||
if (!this.player) { return ' '; }
|
|
||||||
|
|
||||||
return this.player.firstName.substring(0, 1) + this.player.lastName.substring(0, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
<h1 style="text-align: center;">{{ player.firstName }} {{ player.lastName }}</h1>
|
|
||||||
|
|
||||||
<div class="body">
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<mat-slide-toggle [checked]="player.active" (change)="setPlayerActive($event)" [disabled]="!isAdmin" >Spieler aktiv</mat-slide-toggle>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
:host {
|
|
||||||
display: flex;
|
|
||||||
overflow: hidden;
|
|
||||||
flex-direction: column;
|
|
||||||
padding: 8px 14px 8px 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.body {
|
|
||||||
padding: 6px 12px 24px 6px;
|
|
||||||
}
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
|
||||||
import { HttpClient } from '@angular/common/http';
|
|
||||||
import { Component, EventEmitter, inject, Inject, Output } from '@angular/core';
|
|
||||||
import { MAT_DIALOG_DATA } from '@angular/material/dialog';
|
|
||||||
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
|
|
||||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
|
||||||
import { Player } from '../../../../../../model';
|
|
||||||
import { AuthService } from 'src/app/modules/auth.service';
|
|
||||||
import { environment } from 'src/environments/environment';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-player-details',
|
|
||||||
templateUrl: './player-details.component.html',
|
|
||||||
styleUrls: ['./player-details.component.scss'],
|
|
||||||
standalone: true,
|
|
||||||
imports: [CommonModule, MatSlideToggleModule]
|
|
||||||
})
|
|
||||||
export class PlayerDetailsComponent {
|
|
||||||
|
|
||||||
player: Player;
|
|
||||||
loading: boolean = false;
|
|
||||||
private auth: AuthService = inject(AuthService);
|
|
||||||
|
|
||||||
@Output('onReload') changed = new EventEmitter();
|
|
||||||
|
|
||||||
constructor(@Inject(MAT_DIALOG_DATA) public data: DialogData, private http: HttpClient, private snackBar: MatSnackBar) {
|
|
||||||
this.player = data.player;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
get isAdmin() {
|
|
||||||
return this.auth.isAdmin;
|
|
||||||
}
|
|
||||||
|
|
||||||
setPlayerActive(event: any) {
|
|
||||||
this.loading = true;
|
|
||||||
const copy = { ...this.player }
|
|
||||||
this.player.active = event.checked;
|
|
||||||
this.http.put<Player>(`${environment.apiUrl}teams/${this.player.team.id}/players`, this.player).subscribe({
|
|
||||||
next: r => {
|
|
||||||
this.changed.emit();
|
|
||||||
},
|
|
||||||
error: e => {
|
|
||||||
this.player.active = copy.active;
|
|
||||||
|
|
||||||
this.snackBar.open('Spieler konnte nicht aktualisiert werden', undefined, {
|
|
||||||
duration: 5000,
|
|
||||||
panelClass: 'snackbar_error'
|
|
||||||
})
|
|
||||||
|
|
||||||
},
|
|
||||||
complete: () => { this.loading = false; }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DialogData {
|
|
||||||
player: Player;
|
|
||||||
}
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
<div class="card" *ngIf="team">
|
|
||||||
<div class="title-row">
|
|
||||||
<span class="name" id="teamName">{{ team.name }} ({{ translateRolename(roleName) }})</span>
|
|
||||||
<div>Spieleranzahl: {{ team.players?.length }} (aktiv: {{ activePlayerCount }}) </div>
|
|
||||||
</div>
|
|
||||||
<div class="flex-row">
|
|
||||||
<div>Saldo: {{ team.balance | currency:'EUR' }}</div>
|
|
||||||
<div>Ausstehend: {{ team.outstanding | currency:'EUR' }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<mat-divider style="margin-top: 4px"></mat-divider>
|
|
||||||
<div class="flex-row" style="margin-top: 4px;">
|
|
||||||
|
|
||||||
<button mat-icon-button color="primary" aria-label="Back" (click)="back()" matTooltip="Zurück">
|
|
||||||
<mat-icon>arrow_back</mat-icon>
|
|
||||||
</button>
|
|
||||||
<button mat-button mat-raised-button color="primary" (click)="openTeamTransactionDialog()" [disabled]="isLoading || !canDoTransactions">Buchung</button>
|
|
||||||
<button mat-button mat-stroked-button color="accent" (click)="onAllClick()" [disabled]="isLoading || !canDoTransactions" matTooltip="Buchung für alle Spieler anlegen">Umlage</button>
|
|
||||||
|
|
||||||
|
|
||||||
<button mat-icon-button color="primary" matTooltip="Verwaltung" [matMenuTriggerFor]="menu" [disabled]="isLoading">
|
|
||||||
<mat-icon>menu</mat-icon>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<mat-tab-group>
|
|
||||||
<mat-tab label="Buchungen">
|
|
||||||
<ng-container *ngTemplateOutlet="playerList"></ng-container>
|
|
||||||
</mat-tab>s
|
|
||||||
|
|
||||||
<mat-tab label="Bearbeiten">
|
|
||||||
<div class="mat-tab__content flex-row break" style="overflow: auto;">
|
|
||||||
<app-player-card *ngFor="let player of team?.players" class="card" [player]="player"
|
|
||||||
matRipple (click)="openPlayerdetails(player)" (onReload)="loadTeamDetails()" ></app-player-card>
|
|
||||||
</div>
|
|
||||||
</mat-tab>
|
|
||||||
</mat-tab-group>
|
|
||||||
|
|
||||||
|
|
||||||
<ng-template #playerList>
|
|
||||||
<div class="mat-tab__content">
|
|
||||||
<div class="flex-row">
|
|
||||||
<div></div>
|
|
||||||
<div class="select_all" (click)="selectAll()">Alle</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card list">
|
|
||||||
<mat-form-field style="width: 100%;" (keyup)="filter($event)">
|
|
||||||
<mat-label>Suche</mat-label>
|
|
||||||
<input type="text" matInput >
|
|
||||||
</mat-form-field>
|
|
||||||
<mat-selection-list #players id="playerlist" [(ngModel)]="selectedPlayers">
|
|
||||||
<ng-container *ngFor="let p of team?.players" >
|
|
||||||
|
|
||||||
<mat-list-option [value]="p" *ngIf="!p.hide && p.active">
|
|
||||||
<div matListItemTitle [class.highlight]="p.usersPlayer">{{ p.firstName}} {{ p.lastName }} <span *ngIf="p.usersPlayer">- Ich</span> </div>
|
|
||||||
<div matListItemLine>{{ p.balance | currency:'EUR'}}</div>
|
|
||||||
|
|
||||||
</mat-list-option>
|
|
||||||
|
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
</mat-selection-list>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ng-template>
|
|
||||||
|
|
||||||
<div class="card">
|
|
||||||
|
|
||||||
<div class="flex-row" style="height: 48px; overflow: hidden; justify-content: flex-end;" *ngIf="isLoading">
|
|
||||||
<mat-spinner diameter="38"></mat-spinner>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex-row" *ngIf="!isLoading">
|
|
||||||
|
|
||||||
<span *ngIf="players && players.selectedOptions.selected.length > 0">{{players.selectedOptions.selected.length}} Spieler gewählt</span>
|
|
||||||
<span *ngIf="players && players.selectedOptions.selected.length == 0">Spieler für einen Eintrag wählen</span>
|
|
||||||
<button mat-button mat-raised-button color="primary" (click)="openDialog()"
|
|
||||||
[disabled]="players.selectedOptions.selected.length == 0 || !canDoTransactions" id="buttonNewTransaction"
|
|
||||||
matTooltip="Transaktionen für die gewählten Spieler anlegen">Buchung anlegen</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<mat-menu #menu="matMenu">
|
|
||||||
<button mat-menu-item (click)="onAddPlayerClick()" [disabled]="!canInvite">Spieler hinzufügen</button>
|
|
||||||
<button mat-menu-item (click)="onCreateLink()" [disabled]="selectedPlayers.length != 1 || !canInvite">Registrierungslink erstellen</button>
|
|
||||||
<button mat-menu-item (click)="onShowPrivileges()">Berechtigungen</button>
|
|
||||||
<button mat-menu-item (click)="onShowTeamTransactions()">Transaktionshistorie</button>
|
|
||||||
</mat-menu>
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
.title-row {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
:host {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
overflow: hidden;
|
|
||||||
flex: 1 1 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.list{
|
|
||||||
flex: 1 1 100%;
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.back {
|
|
||||||
width: 28px;
|
|
||||||
height: 28px;
|
|
||||||
background-color: red;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.select_all {
|
|
||||||
margin-right: 52px;
|
|
||||||
text-decoration: underline;
|
|
||||||
cursor: pointer;
|
|
||||||
color: #646464;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: black;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.buttons {
|
|
||||||
margin-top: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.highlight {
|
|
||||||
// text-decoration: wavy;
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
mat-tab-group {
|
|
||||||
flex: 1 1 100%;
|
|
||||||
height: 257px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mat-tab__content {
|
|
||||||
height: 100%;
|
|
||||||
overflow: hidden;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.break {
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
import { HttpClient } from '@angular/common/http';
|
|
||||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
|
||||||
import { MatListModule } from '@angular/material/list';
|
|
||||||
import { By } from '@angular/platform-browser';
|
|
||||||
import { ActivatedRoute } from '@angular/router';
|
|
||||||
import { of } from 'rxjs';
|
|
||||||
|
|
||||||
import { TeamDetailsComponent } from './team-details.component';
|
|
||||||
|
|
||||||
describe('TeamDetailsComponent', () => {
|
|
||||||
let component: TeamDetailsComponent;
|
|
||||||
let fixture: ComponentFixture<TeamDetailsComponent>;
|
|
||||||
const paramMap = jasmine.createSpyObj('ParamMap', ['get'])
|
|
||||||
const fakeActivatedRoute = {
|
|
||||||
snapshot: { paramMap: paramMap }
|
|
||||||
}
|
|
||||||
|
|
||||||
paramMap.get.and.returnValue('9999');
|
|
||||||
|
|
||||||
const mockhttp = jasmine.createSpyObj('HttpClient', ['get']);
|
|
||||||
mockhttp.get.and.returnValue(of({
|
|
||||||
alias: "9999",
|
|
||||||
balance: 48.09,
|
|
||||||
name: "Development Team",
|
|
||||||
outstanding: -582.00,
|
|
||||||
players: [{id: 1, firstName: 'Filiberto', lastName: 'Spencer', balance: -32.97}, {id: 2, firstName: 'Adam', lastName: 'West', balance: -50}],
|
|
||||||
settings: []
|
|
||||||
}))
|
|
||||||
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
await TestBed.configureTestingModule({
|
|
||||||
declarations: [ TeamDetailsComponent ],
|
|
||||||
providers: [
|
|
||||||
{provide: ActivatedRoute, useValue: fakeActivatedRoute},
|
|
||||||
{provide: HttpClient, useValue: mockhttp},
|
|
||||||
],
|
|
||||||
imports: [ MatListModule ]
|
|
||||||
})
|
|
||||||
.compileComponents();
|
|
||||||
|
|
||||||
fixture = TestBed.createComponent(TeamDetailsComponent);
|
|
||||||
component = fixture.componentInstance;
|
|
||||||
fixture.detectChanges();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should create', () => {
|
|
||||||
expect(component).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should extract the id from the url', () => {
|
|
||||||
expect(component.id).toEqual("9999");
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should load the teams data', () => {
|
|
||||||
expect(component.team).not.toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should display name and alias', () => {
|
|
||||||
const nameElement = fixture.debugElement.query(By.css('#teamName'));
|
|
||||||
const text = (nameElement.nativeElement as HTMLElement).textContent;
|
|
||||||
expect(text).toEqual(`${component.team.name} (${component.team.alias})`)
|
|
||||||
|
|
||||||
// const htmlList = (list.nativeElement as HTMLElement).children;
|
|
||||||
// expect(htmlList).not.toBeNull();
|
|
||||||
// expect(htmlList.length).toBe(4)
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should display a list with players', () => {
|
|
||||||
const element = fixture.debugElement.query(By.css('#playerlist'));
|
|
||||||
const ch = (element.nativeElement as HTMLElement).children;
|
|
||||||
expect(ch.length).toBe(component.team.players.length);
|
|
||||||
for (let index = 0; index < component.team.players.length; index++) {
|
|
||||||
const playerName = ch[index].textContent?.trim();
|
|
||||||
const expectedName = `${component.team.players[index].firstName} ${component.team.players[index].lastName}`;
|
|
||||||
expect(playerName).toEqual(expectedName)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it('button should be disabled', () => {
|
|
||||||
const element = fixture.debugElement.query(By.css('#buttonNewTransaction'));
|
|
||||||
expect(element.nativeElement.disabled).toBeTrue();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should enable the button when selecting a player', () => {
|
|
||||||
const element = fixture.debugElement.query(By.css('.mdc-checkbox'));
|
|
||||||
element.nativeElement.click();
|
|
||||||
fixture.detectChanges();
|
|
||||||
const buttonelement = fixture.debugElement.query(By.css('#buttonNewTransaction'));
|
|
||||||
expect(buttonelement.nativeElement.disabled).toBeFalse();
|
|
||||||
|
|
||||||
})
|
|
||||||
});
|
|
||||||
@@ -1,303 +0,0 @@
|
|||||||
import { Component, createNgModuleRef, Injector, OnInit, ViewChild } from '@angular/core';
|
|
||||||
import { MatDialog } from '@angular/material/dialog';
|
|
||||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
|
||||||
import { Player } from 'src/app/model';
|
|
||||||
import { AuthService } from 'src/app/modules/auth.service';
|
|
||||||
import { Team } from 'src/app/modules/teams/model/team';
|
|
||||||
import { TeamInvite } from 'src/app/modules/teams/model/team-invite';
|
|
||||||
import { TeamsService } from 'src/app/modules/teams/teams.service';
|
|
||||||
import { NewTransactionDialogComponent, TeamTransactionDialogComponent } from 'src/app/shared';
|
|
||||||
import { CreatePlayerDialogComponent } from 'src/app/shared/dialog/create-player-dialog/create-player.dialog.component';
|
|
||||||
import { PrivilegesInfoDialogComponent } from 'src/app/shared/dialog/privileges-info-dialog/privileges-info-dialog.component';
|
|
||||||
import { PlayerDetailsComponent } from './player-card/player-details/player-details.component';
|
|
||||||
import { CommonModule } from '@angular/common';
|
|
||||||
import { MatDividerModule } from '@angular/material/divider';
|
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
|
||||||
import { MatIconModule } from '@angular/material/icon';
|
|
||||||
import { MatMenuModule } from '@angular/material/menu';
|
|
||||||
import { MatTabsModule } from '@angular/material/tabs';
|
|
||||||
import { PlayerCardComponent } from './player-card/player-card.component';
|
|
||||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
|
||||||
import { MatListModule } from '@angular/material/list';
|
|
||||||
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
|
|
||||||
import { MatInputModule } from '@angular/material/input';
|
|
||||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
|
||||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-team-details',
|
|
||||||
templateUrl: './team-details.component.html',
|
|
||||||
styleUrls: ['./team-details.component.scss'],
|
|
||||||
standalone: true,
|
|
||||||
imports: [
|
|
||||||
CommonModule
|
|
||||||
, ReactiveFormsModule
|
|
||||||
, FormsModule
|
|
||||||
, MatProgressSpinnerModule
|
|
||||||
, MatDividerModule
|
|
||||||
, MatButtonModule
|
|
||||||
, MatIconModule
|
|
||||||
, MatMenuModule
|
|
||||||
, MatTabsModule
|
|
||||||
, MatFormFieldModule
|
|
||||||
, MatListModule
|
|
||||||
, MatSlideToggleModule
|
|
||||||
, MatInputModule
|
|
||||||
, PlayerCardComponent
|
|
||||||
]
|
|
||||||
})
|
|
||||||
export class TeamDetailsComponent implements OnInit {
|
|
||||||
id: string | null = null;
|
|
||||||
|
|
||||||
team: Team | undefined;
|
|
||||||
|
|
||||||
correspondingPlayer: Player | undefined;
|
|
||||||
|
|
||||||
canDoTransactions: boolean = false;
|
|
||||||
canInvite: boolean = false;
|
|
||||||
roleName: string = '';
|
|
||||||
|
|
||||||
isLoading: boolean = true;
|
|
||||||
|
|
||||||
selectedPlayers: Player[] = [];
|
|
||||||
|
|
||||||
usersPlayers: Player[] = [];
|
|
||||||
|
|
||||||
@ViewChild('players') players: any;
|
|
||||||
|
|
||||||
constructor(private route: ActivatedRoute, private teamsService: TeamsService, private authService: AuthService,
|
|
||||||
private router: Router, public dialog: MatDialog, private injector: Injector, private _snackBar: MatSnackBar) {}
|
|
||||||
|
|
||||||
ngOnInit(): void {
|
|
||||||
this.id = this.route.snapshot.paramMap.get('id');
|
|
||||||
|
|
||||||
if (this.id) {
|
|
||||||
this.loadTeamDetails();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected loadTeamDetails() {
|
|
||||||
if (!this.id) { return; }
|
|
||||||
this.teamsService.loadTeamDetails(this.id).subscribe(result => {
|
|
||||||
this.team = result;
|
|
||||||
this.findCorrespondingPlayer();
|
|
||||||
this.isLoading = false;
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
back() {
|
|
||||||
this.router.navigate(['../dashboard'])
|
|
||||||
}
|
|
||||||
|
|
||||||
openDialog(): void {
|
|
||||||
|
|
||||||
const dialogRef = this.dialog.open(NewTransactionDialogComponent, {
|
|
||||||
data: {
|
|
||||||
team: this.team,
|
|
||||||
players: this.selectedPlayers
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
dialogRef.afterClosed().subscribe(async result => {
|
|
||||||
if (result && result.length > 0) {
|
|
||||||
this.isLoading = true;
|
|
||||||
this.teamsService.createTransactions(result).subscribe(res => {
|
|
||||||
if (res && res.length > 0) {
|
|
||||||
this.loadTeamDetails();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
openTeamTransactionDialog() {
|
|
||||||
if (!this.team) { return; }
|
|
||||||
const dialogRef = this.dialog.open(TeamTransactionDialogComponent, {
|
|
||||||
data: {
|
|
||||||
teamId: this.team.id
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
dialogRef.afterClosed().subscribe(async result => {
|
|
||||||
|
|
||||||
if (result) { this.saveTeamTransaction(result); }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
saveTeamTransaction(transaction: any) {
|
|
||||||
this.isLoading = true;
|
|
||||||
this.teamsService.createTeamTransaction(transaction).subscribe(res => {
|
|
||||||
if (res && res['id']) {
|
|
||||||
this.loadTeamDetails();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
selectAll() {
|
|
||||||
if (this.team == null || this.team.players == null) { return; }
|
|
||||||
if (this.selectedPlayers.length < this.team.players.length) {
|
|
||||||
this.selectedPlayers = [];
|
|
||||||
for (let p of this.team.players) {
|
|
||||||
this.selectedPlayers.push(p);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.selectedPlayers = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
onAllClick() {
|
|
||||||
if (!this.team) { return; }
|
|
||||||
const dialogRef = this.dialog.open(NewTransactionDialogComponent, {
|
|
||||||
data: {
|
|
||||||
team: this.team,
|
|
||||||
players: this.selectedPlayers,
|
|
||||||
all: this.team.players?.length == this.selectedPlayers.length
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onAddPlayerClick() {
|
|
||||||
if (!this.team) { return; }
|
|
||||||
const dialogRef = this.dialog.open(CreatePlayerDialogComponent, {
|
|
||||||
data: { },
|
|
||||||
width: '350px'
|
|
||||||
});
|
|
||||||
|
|
||||||
dialogRef.afterClosed().subscribe(result => {
|
|
||||||
if (result && this.team) {
|
|
||||||
this.teamsService.createNewPlayer(this.team.id, result).subscribe({
|
|
||||||
next: () => { this.loadTeamDetails()},
|
|
||||||
error: () => { this.onPlayerCreateError();}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
onPlayerCreateError() {
|
|
||||||
this._snackBar.open('Spieler konnte nicht erstellt werden', undefined, {
|
|
||||||
duration: 10000,
|
|
||||||
panelClass: 'snackbar_error'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async onCreateLink() {
|
|
||||||
if (!this.team) { return; }
|
|
||||||
|
|
||||||
try {
|
|
||||||
const invite: TeamInvite = {
|
|
||||||
teamId: this.team.id,
|
|
||||||
teamName: this.team.name,
|
|
||||||
playerId: this.selectedPlayers[0].id,
|
|
||||||
playerName: this.selectedPlayers[0].firstName + ' ' + this.selectedPlayers[0].lastName
|
|
||||||
}
|
|
||||||
const token = await this.getInviteFromServer(invite);
|
|
||||||
|
|
||||||
const link = location.origin + '/auth/register/' + token
|
|
||||||
|
|
||||||
await navigator.clipboard.writeText(link);
|
|
||||||
this._snackBar.open('Einladungslink für ' + invite.playerName + ' in die Zwischenablage kopiert. Der Link ist auf den gewählten Spieler personalisiert und darf nicht an mehrere Spieler verteilt werden.', undefined, {
|
|
||||||
duration: 10000,
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch {
|
|
||||||
this._snackBar.open('Der Einladungslink konnte nicht erzeugt werden.', undefined, {
|
|
||||||
panelClass: 'snackbar_error',
|
|
||||||
duration: 5000,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async getInviteFromServer(invite: TeamInvite): Promise<any> {
|
|
||||||
return new Promise<any>((resolve, reject) => {
|
|
||||||
this.teamsService.createRegisterLink(invite).subscribe({
|
|
||||||
next: token => { return resolve(token.token); },
|
|
||||||
error: () => { return reject(null); }
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
findCorrespondingPlayer() {
|
|
||||||
if (!this.team || !this.team.players) { return; }
|
|
||||||
const id = this.authService.user?.id;
|
|
||||||
this.usersPlayers = this.team.players.filter((p: any) => { return p.user?.id == id });
|
|
||||||
this.canInvite = this.authService.isAdmin || this.usersPlayers.find(p => p.teamRole.id > 2) != null;
|
|
||||||
this.canDoTransactions = this.authService.isAdmin || this.usersPlayers.find(p => p.teamRole.id >= 2) != null;
|
|
||||||
|
|
||||||
this.roleName = this.usersPlayers.reduce((prev, curr) => { return (prev.teamRole.id > curr.teamRole.id) ? prev : curr})?.teamRole.name;
|
|
||||||
|
|
||||||
for (let u of this.usersPlayers) {
|
|
||||||
u.usersPlayer = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onShowPrivileges() {
|
|
||||||
const dialogRef = this.dialog.open(PrivilegesInfoDialogComponent, {
|
|
||||||
data: null
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
translateRolename(roleName: string): string {
|
|
||||||
switch (roleName) {
|
|
||||||
case 'treasurer':
|
|
||||||
return 'Kassenwart'
|
|
||||||
case 'scnd_treasurer':
|
|
||||||
return 'zweiter Kassenwart'
|
|
||||||
case 'captain':
|
|
||||||
return 'Kapitän'
|
|
||||||
case 'coach':
|
|
||||||
return 'Trainer'
|
|
||||||
default:
|
|
||||||
return 'Spieler'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
async onShowTeamTransactions() {
|
|
||||||
if (!this.team) { return; }
|
|
||||||
|
|
||||||
const { ShowTeamTransactionsComponent } = await import(
|
|
||||||
'src/app/shared/dialog/show-team-transactions/show-team-transactions.component'
|
|
||||||
);
|
|
||||||
|
|
||||||
this.dialog.open(ShowTeamTransactionsComponent, {
|
|
||||||
data: {
|
|
||||||
id: this.team.id
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
openPlayerdetails(player: Player) {
|
|
||||||
if (!player) { return; }
|
|
||||||
const dialogRef = this.dialog.open(PlayerDetailsComponent, {
|
|
||||||
data: {
|
|
||||||
player
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
dialogRef.componentInstance.changed.subscribe(() => {
|
|
||||||
this.loadTeamDetails();
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
filter(event: any) {
|
|
||||||
const search = event.target.value;
|
|
||||||
const s = search.toLowerCase().trim();
|
|
||||||
this.team?.players?.map(p => {
|
|
||||||
const n = p.firstName.toLowerCase().trim() + '' + p.lastName.toLowerCase().trim();
|
|
||||||
p.hide = !n.includes(s);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
get activePlayerCount(): number {
|
|
||||||
if (!this.team || !this.team.players) { return 0; }
|
|
||||||
return this.team.players.filter(p => p.active).length;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
<mat-toolbar color="accent" class="toolbar">
|
|
||||||
<span>{{ username }}</span>
|
|
||||||
|
|
||||||
<button mat-button (click)="logout()" *ngIf="isLoggedIn">Logout</button>
|
|
||||||
<button mat-button (click)="login()" *ngIf="!isLoggedIn">Login</button>
|
|
||||||
</mat-toolbar>
|
|
||||||
<div class="content_container">
|
|
||||||
<router-outlet></router-outlet>
|
|
||||||
</div>
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
.content_container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
padding: 8px;
|
|
||||||
flex: 1 1 auto;
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
.card {
|
|
||||||
padding: 12px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
:host {
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
import { Component, inject } from '@angular/core';
|
|
||||||
import { Router, RouterModule } from '@angular/router';
|
|
||||||
import { UserService } from 'src/app/core/user/user.service';
|
|
||||||
import { AuthService } from '../auth.service';
|
|
||||||
import { CommonModule } from '@angular/common';
|
|
||||||
import { MatToolbarModule } from '@angular/material/toolbar';
|
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-home',
|
|
||||||
templateUrl: './home.component.html',
|
|
||||||
styleUrls: ['./home.component.scss'],
|
|
||||||
standalone: true,
|
|
||||||
imports: [
|
|
||||||
CommonModule,
|
|
||||||
MatToolbarModule,
|
|
||||||
RouterModule,
|
|
||||||
MatButtonModule
|
|
||||||
]
|
|
||||||
})
|
|
||||||
export class HomeComponent {
|
|
||||||
private authService: AuthService = inject(AuthService);
|
|
||||||
private userService: UserService = inject(UserService);
|
|
||||||
private router: Router = inject(Router);
|
|
||||||
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
) {}
|
|
||||||
|
|
||||||
|
|
||||||
ngOnInit(): void {
|
|
||||||
this.loginFromLocalStorage();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
get username(): string {
|
|
||||||
if (!this.authService || !this.authService.isLoggedIn()) { return ''}
|
|
||||||
return this.authService.user.firstName + ' ' + this.authService.user.lastName
|
|
||||||
}
|
|
||||||
|
|
||||||
logout() {
|
|
||||||
this.authService.logout();
|
|
||||||
}
|
|
||||||
|
|
||||||
get isLoggedIn(): boolean {
|
|
||||||
return this.authService.isLoggedIn()
|
|
||||||
}
|
|
||||||
|
|
||||||
private async loginFromLocalStorage() {
|
|
||||||
const success = await this.authService.loginFromLocalStorage();
|
|
||||||
|
|
||||||
if (!success) {
|
|
||||||
this.router.navigate(['/auth']).then();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.authService && this.authService.isLoggedIn()) {
|
|
||||||
this.userService.loadTeamsOfUser();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.router.url == '/') {
|
|
||||||
this.router.navigate(['/dashboard']).then();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async login() {
|
|
||||||
|
|
||||||
|
|
||||||
// this.router.navigate(['/auth']).then();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import { AuthGuard } from 'src/app/core/guards/auth.guard';
|
|
||||||
import { Route } from '@angular/router';
|
|
||||||
import { dashboardRoutes } from './dashboard/dashboard.routes';
|
|
||||||
|
|
||||||
export const homeRoutes: Route[] = [
|
|
||||||
{ path: '', loadComponent: () => import('./home.component').then(m => m.HomeComponent), children: [
|
|
||||||
{ path: 'dashboard', canLoad: [AuthGuard], children: dashboardRoutes },
|
|
||||||
] },
|
|
||||||
];
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
export interface TeamInvite {
|
|
||||||
teamId: number;
|
|
||||||
teamName: string;
|
|
||||||
playerId: number;
|
|
||||||
playerName: string;
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import { Player } from 'src/app/model';
|
|
||||||
|
|
||||||
export interface Team {
|
|
||||||
id: number;
|
|
||||||
alias: string;
|
|
||||||
balance: number;
|
|
||||||
name: string;
|
|
||||||
outstanding?: number;
|
|
||||||
players?: Player[];
|
|
||||||
settings?: any[];
|
|
||||||
}
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
<!-- <div *ngFor="let transaction of transactions" class="row">
|
|
||||||
<div class="amount">{{transaction.amount | currency }}</div>
|
|
||||||
<div class="date"> {{ transaction.date | date }}</div>
|
|
||||||
<div class="date"> {{ transaction.createdAt | date }}</div>
|
|
||||||
<div class="date"> {{ transaction.note }}</div>
|
|
||||||
|
|
||||||
</div> -->
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<div id="userBalance" class="balance" [ngClass]="{'pos': data.user.balance >= 0, 'neg': data.user.balance < 0}"><span style="font-weight: 300">{{ data.user.firstName }}:</span> {{ data.user.balance | currency:'EUR' }}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@if(isLoading) {
|
|
||||||
<mat-spinner style="align-self: center; margin: 48px;"></mat-spinner>
|
|
||||||
} @else if (transactions.length > 0) {
|
|
||||||
<mat-list class="list" id="transactionsList">
|
|
||||||
|
|
||||||
<mat-list-item *ngFor="let transaction of transactions">
|
|
||||||
<span matListItemTitle>{{ transaction.date | date }}</span>
|
|
||||||
<span matListItemLine>
|
|
||||||
<span class="amount" [class.negative]="transaction.type.id > 9">
|
|
||||||
<ng-container *ngIf="transaction.type.id > 9">-</ng-container>
|
|
||||||
{{transaction.amount | currency:'EUR' }}
|
|
||||||
</span>
|
|
||||||
{{ transaction.type.name | translate }}
|
|
||||||
-
|
|
||||||
{{ transaction.note }}
|
|
||||||
</span>
|
|
||||||
</mat-list-item>
|
|
||||||
|
|
||||||
|
|
||||||
</mat-list>
|
|
||||||
} @else {
|
|
||||||
<div style="align-self: center; margin: 48px;">Es sind keine Einträge vorhanden</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<!-- <div>
|
|
||||||
<div class="actions">
|
|
||||||
<div>Für <span class="name">{{ data.user.firstName}}</span></div>
|
|
||||||
<div class="actions__buttons">
|
|
||||||
<button mat-flat-button color="primary" disabled>Beantragen</button>
|
|
||||||
<button mat-flat-button color="warn" disabled>Beanstanden</button>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div> -->
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
:host {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
height: 100%;
|
|
||||||
overflow: hidden;
|
|
||||||
min-height: 200px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.row {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.list {
|
|
||||||
max-height: 500px;
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.amount {
|
|
||||||
color: rgb(49, 119, 49);
|
|
||||||
}
|
|
||||||
|
|
||||||
.negative {
|
|
||||||
color: rgb(202, 62, 62);
|
|
||||||
}
|
|
||||||
|
|
||||||
.actions {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding-left: 8px;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
.name {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__buttons {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
padding-right: 2px;
|
|
||||||
button {
|
|
||||||
margin: 4px 2px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.balance {
|
|
||||||
|
|
||||||
color: #fff;
|
|
||||||
padding: 12px;
|
|
||||||
text-align: center;
|
|
||||||
font-size: 1.5rem;
|
|
||||||
|
|
||||||
&.pos {
|
|
||||||
background-color: rgb(49, 119, 49);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.neg {
|
|
||||||
background-color: rgb(202, 62, 62);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
import { HttpClientModule } from '@angular/common/http';
|
|
||||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
|
||||||
import { MatDialogModule, MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
|
|
||||||
import { MatListModule } from '@angular/material/list';
|
|
||||||
import { By } from '@angular/platform-browser';
|
|
||||||
import { TeamsService } from '../../teams.service';
|
|
||||||
|
|
||||||
import { DetailsComponent } from './details.component';
|
|
||||||
|
|
||||||
describe('DetailsComponent', () => {
|
|
||||||
let component: DetailsComponent;
|
|
||||||
let fixture: ComponentFixture<DetailsComponent>;
|
|
||||||
const mockTeamService = jasmine.createSpyObj('TeamsService', ['loadTransactionsOfUser']);
|
|
||||||
mockTeamService.selectedUserTransaction = [
|
|
||||||
{id: 1, note: 'dev1', createdAt: '', date: '', amount: 100, type: {id: 1}},
|
|
||||||
{id: 2, note: 'dev2', createdAt: '', date: '', amount: 100, type: {id: 1}},
|
|
||||||
{id: 3, note: 'dev3', createdAt: '', date: '', amount: 100, type: {id: 1}},
|
|
||||||
{id: 4, note: 'dev4', createdAt: '', date: '', amount: 100, type: {id: 1}},
|
|
||||||
];
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
await TestBed.configureTestingModule({
|
|
||||||
declarations: [ DetailsComponent ],
|
|
||||||
imports: [ MatDialogModule, HttpClientModule, MatListModule ],
|
|
||||||
providers: [
|
|
||||||
{ provide: MAT_DIALOG_DATA, useValue: {} },
|
|
||||||
{ provide: MatDialogRef, useValue: {} },
|
|
||||||
{ provide: TeamsService, useValue: mockTeamService }
|
|
||||||
]
|
|
||||||
})
|
|
||||||
.compileComponents();
|
|
||||||
|
|
||||||
fixture = TestBed.createComponent(DetailsComponent);
|
|
||||||
|
|
||||||
component = fixture.componentInstance;
|
|
||||||
|
|
||||||
component.data.user = {
|
|
||||||
id: 2,
|
|
||||||
balance: 46.64,
|
|
||||||
firstName: 'Rover',
|
|
||||||
lastName: 'Stiedeman',
|
|
||||||
user: null
|
|
||||||
}
|
|
||||||
|
|
||||||
fixture.detectChanges();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should create', () => {
|
|
||||||
expect(component).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should initialise the user', () => {
|
|
||||||
|
|
||||||
|
|
||||||
expect(component.data.user).not.toBeNull();
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should have a users transactions', () => {
|
|
||||||
expect(component.transactions).not.toBeNull();
|
|
||||||
expect(component.transactions.length).toBe(4)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should display the users balance', () => {
|
|
||||||
const balance = 46.64;
|
|
||||||
component.data.user.balance = balance;
|
|
||||||
const el = fixture.debugElement.query(By.css('#userBalance'));
|
|
||||||
expect(el).not.toBeNull();
|
|
||||||
expect(el.nativeElement.textContent.trim()).toEqual('Rover: €' + balance);
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should display users transactions', () => {
|
|
||||||
const list = fixture.debugElement.query(By.css('#transactionsList'));
|
|
||||||
const htmlList = (list.nativeElement as HTMLElement).children;
|
|
||||||
expect(htmlList).not.toBeNull();
|
|
||||||
expect(htmlList.length).toBe(4)
|
|
||||||
|
|
||||||
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
import { Component, Inject, OnInit } from '@angular/core';
|
|
||||||
import { MatDialogRef, MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';
|
|
||||||
import { TeamsService } from '../../teams.service';
|
|
||||||
import {MatListModule} from '@angular/material/list';
|
|
||||||
import {MatProgressSpinnerModule} from '@angular/material/progress-spinner';
|
|
||||||
import { CommonModule } from '@angular/common';
|
|
||||||
import { TranslateModule, TranslatePipe } from '@ngx-translate/core';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-details',
|
|
||||||
templateUrl: './details.component.html',
|
|
||||||
styleUrls: ['./details.component.scss'],
|
|
||||||
standalone: true,
|
|
||||||
imports: [CommonModule, MatDialogModule, MatListModule, MatProgressSpinnerModule, TranslateModule],
|
|
||||||
providers: []
|
|
||||||
})
|
|
||||||
export class DetailsComponent implements OnInit {
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
public dialogRef: MatDialogRef<DetailsComponent>,
|
|
||||||
@Inject(MAT_DIALOG_DATA) public data: { user: any },
|
|
||||||
private teamService: TeamsService
|
|
||||||
) {}
|
|
||||||
|
|
||||||
ngOnInit(): void {
|
|
||||||
console.log("Onmit")
|
|
||||||
this.teamService.loadTransactionsOfUser(this.data.user.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
get transactions(): any[] {
|
|
||||||
return this.teamService.selectedUserTransaction;
|
|
||||||
}
|
|
||||||
|
|
||||||
get isLoading(): boolean {
|
|
||||||
return this.teamService.transactionsLoading;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
import { NgModule } from '@angular/core';
|
|
||||||
import { RouterModule, Routes } from '@angular/router';
|
|
||||||
import { OverviewComponent } from './overview.component';
|
|
||||||
|
|
||||||
const routes: Routes = [
|
|
||||||
{
|
|
||||||
path: '', component: OverviewComponent, pathMatch: 'full'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
@NgModule({
|
|
||||||
imports: [RouterModule.forChild(routes)],
|
|
||||||
exports: [RouterModule]
|
|
||||||
})
|
|
||||||
export class OverviewRoutingModule { }
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
<mat-toolbar color="primary" class="toolbar">
|
|
||||||
|
|
||||||
<span>{{ teamsService.team?.name }}</span>
|
|
||||||
<button mat-button (click)="onLoginClick()">Login</button>
|
|
||||||
|
|
||||||
</mat-toolbar>
|
|
||||||
|
|
||||||
<div class="team-info mat-elevation-z2">
|
|
||||||
<ng-container *ngIf="teamsService && teamsService.team">
|
|
||||||
<div style="text-align: center;" matTooltip="Aktuell in der Kasse">
|
|
||||||
Kontostand: {{ teamsService.team.balance | currency:'EUR' }}
|
|
||||||
</div>
|
|
||||||
<div style="text-align: center;"
|
|
||||||
[matTooltip]="teamsService.team.outstanding > 0 ? 'Die Spieler schulden der Mannschaftskasse diesen Betrag '
|
|
||||||
: 'Die Mannschaftskasse schuldet den Spielern diesen Betrag'">
|
|
||||||
Ausstehend: {{ teamsService.team.outstanding | currency:'EUR' }}
|
|
||||||
</div>
|
|
||||||
<div style="text-align: center;" matTooltip="Theoretisch in der Kasse">
|
|
||||||
Total: {{ teamsService.team.outstanding + teamsService.team.balance | currency:'EUR' }}
|
|
||||||
</div>
|
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex-row" style="margin: 12px 24px -24px 24px; align-items: flex-start; justify-content: center; gap: 24px">
|
|
||||||
<mat-form-field style="flex: 1 1 auto; " appearance="outline">
|
|
||||||
<mat-label>Suche</mat-label>
|
|
||||||
<input matInput (keyup)="applyFilter($event)" placeholder="Name" #input type="search" (search)="applyFilter($event)">
|
|
||||||
</mat-form-field>
|
|
||||||
|
|
||||||
<button mat-raised-button (click)="openPenalties()">
|
|
||||||
Strafenkatalog
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="table-container mat-elevation-z2">
|
|
||||||
|
|
||||||
|
|
||||||
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8" matSort (matSortChange)="announceSortChange($event)" matSortActive="balance" matSortDirection="asc">
|
|
||||||
|
|
||||||
<!--- Note that these columns can be defined in any order.
|
|
||||||
The actual rendered columns are set as a property on the row definition" -->
|
|
||||||
|
|
||||||
<!-- Position Column -->
|
|
||||||
<ng-container matColumnDef="firstName">
|
|
||||||
<th mat-header-cell *matHeaderCellDef mat-sort-header sortActionDescription="Sort by firstName"> Vorname </th>
|
|
||||||
<td mat-cell *matCellDef="let element"> {{element.firstName}} </td>
|
|
||||||
<td mat-footer-cell *matFooterCellDef> </td>
|
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
<!-- Name Column -->
|
|
||||||
<ng-container matColumnDef="lastName">
|
|
||||||
<th mat-header-cell *matHeaderCellDef mat-sort-header sortActionDescription="Sort by lastName"> Nachname </th>
|
|
||||||
<td mat-cell *matCellDef="let element" [class.blurr]="false"> {{element.lastName}} </td>
|
|
||||||
<td mat-footer-cell *matFooterCellDef> Total </td>
|
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
<!-- Weight Column -->
|
|
||||||
<ng-container matColumnDef="balance">
|
|
||||||
<th mat-header-cell *matHeaderCellDef class="align-end" mat-sort-header sortActionDescription="Sort by number"> Saldo </th>
|
|
||||||
<td mat-cell *matCellDef="let element" class="align-end"> {{element.balance | currency:'EUR' }} </td>
|
|
||||||
<td mat-footer-cell *matFooterCellDef> {{getTotalBalance() | currency:'EUR' }} </td>
|
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<tr mat-header-row *matHeaderRowDef="displayedColumns; sticky: true"></tr>
|
|
||||||
<tr mat-row *matRowDef="let row; columns: displayedColumns;" (click)="openDetails(row)" class="table-row"></tr>
|
|
||||||
<tr mat-footer-row *matFooterRowDef="displayedColumns; sticky: true"></tr>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||