| @@ -0,0 +1,16 @@ | |||||
| # 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 | |||||
| @@ -0,0 +1,42 @@ | |||||
| # 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 | |||||
| @@ -0,0 +1,65 @@ | |||||
| #!/bin/bash | |||||
| export PATH=/opt/plesk/php/8.2/bin:$PATH; | |||||
| cd /var/www/vhosts/spawntree.de/git_repo_clones/futbase-fe/ | |||||
| sudo git pull | |||||
| echo "$(tput setab 2)matsen frontend has been PULLED$(tput sgr 0)" | |||||
| rm -rf /var/www/vhosts/spawntree.de/matsen.spawntree.de/httpdocs/composer.lock | |||||
| #cp -rf /var/www/vhosts/spawntree.de/git_repo_clones/futbase-fe/httpdocs/composer.lock /var/www/vhosts/spawntree.de/matsen.spawntree.de/httpdocs | |||||
| rm -rf /var/www/vhosts/spawntree.de/matsen.spawntree.de/httpdocs/composer.json | |||||
| cp -rf /var/www/vhosts/spawntree.de/git_repo_clones/futbase-fe/composer.json /var/www/vhosts/spawntree.de/matsen.spawntree.de/httpdocs | |||||
| rm -rf /var/www/vhosts/spawntree.de/matsen.spawntree.de/httpdocs/config | |||||
| cp -rf /var/www/vhosts/spawntree.de/git_repo_clones/futbase-fe/config /var/www/vhosts/spawntree.de/matsen.spawntree.de/httpdocs | |||||
| rm -rf /var/www/vhosts/spawntree.de/matsen.spawntree.de/httpdocs/bin | |||||
| cp -rf /var/www/vhosts/spawntree.de/git_repo_clones/futbase-fe/bin /var/www/vhosts/spawntree.de/matsen.spawntree.de/httpdocs | |||||
| rm -rf /var/www/vhosts/spawntree.de/matsen.spawntree.de/httpdocs/migrations | |||||
| cp -rf /var/www/vhosts/spawntree.de/git_repo_clones/futbase-fe/migrations /var/www/vhosts/spawntree.de/matsen.spawntree.de/httpdocs | |||||
| rm -rf /var/www/vhosts/spawntree.de/matsen.spawntree.de/httpdocs/src | |||||
| cp -rf /var/www/vhosts/spawntree.de/git_repo_clones/futbase-fe/src /var/www/vhosts/spawntree.de/matsen.spawntree.de/httpdocs | |||||
| rm -rf /var/www/vhosts/spawntree.de/matsen.spawntree.de/httpdocs/public/index.php | |||||
| cp -rf /var/www/vhosts/spawntree.de/git_repo_clones/futbase-fe/public/index.php /var/www/vhosts/spawntree.de/matsen.spawntree.de/httpdocs/public | |||||
| echo "$(tput setab 2)Files have been copied$(tput sgr 0)" | |||||
| cd /var/www/vhosts/spawntree.de/matsen.spawntree.de/httpdocs | |||||
| composer update --no-scripts | |||||
| echo "$(tput setab 2)COMPOSER UPDATED updated$(tput sgr 0)" | |||||
| php /var/www/vhosts/spawntree.de/matsen.spawntree.de/httpdocs/bin/console doctrine:migrations:migrate | |||||
| echo "$(tput setab 2)DATABASE SCHEMA updated$(tput sgr 0)" | |||||
| cd /var/www/vhosts/spawntree.de/ | |||||
| sudo chmod 777 matsen.spawntree.de | |||||
| cd /var/www/vhosts/spawntree.de/matsen.spawntree.de/ | |||||
| sudo chmod 777 -R * | |||||
| cd /var/www/vhosts/spawntree.de/matsen.spawntree.de/httpdocs/var/cache/ | |||||
| rm -R * | |||||
| php /var/www/vhosts/spawntree.de/matsen.spawntree.de/httpdocs/bin/console cache:clear | |||||
| php /var/www/vhosts/spawntree.de/matsen.spawntree.de/httpdocs/bin/console cache:warmup | |||||
| echo "$(tput setab 2)CACHE HAS BEEN CLEARED$(tput sgr 0)" | |||||
| cd /var/www/vhosts/spawntree.de/matsen.spawntree.de/httpdocs/var/ | |||||
| chmod 777 -R * | |||||
| chmod 777 cache/ * | |||||
| chmod 777 cache/ | |||||
| #service apache2 restart | |||||
| #echo "$(tput setab 2)CACHE cleared$(tput sgr 0)" | |||||
| echo "$(tput setab 7)$(tput setaf 1)THINK ABOUT POSSIBLE PATCHES!" | |||||
| echo "You have updated matsen api!$(tput sgr 0)" | |||||
| @@ -0,0 +1,88 @@ | |||||
| # Futmachine | |||||
| This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 17.0.7. | |||||
| ## 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. | |||||
| ############ | |||||
| # BEFORE Installation: | |||||
| - npm -v (minimum is 8.0.0) | |||||
| - node --version (minimum is 20.9.0) | |||||
| - brew upgrade node | |||||
| - npm install | |||||
| ## Installation | |||||
| - npm i -g @angular/cli | |||||
| ### Only once | |||||
| - ng new futbase --no-standalone | |||||
| - Standalone is now the new default in v17 (no app.module.ts) | |||||
| - cd futbase -> ng serve | |||||
| ## Install Bootstrap | |||||
| - cd futbase | |||||
| - npm i bootstrap @popperjs/core --save | |||||
| - npm install bootstrap-icons | |||||
| ## Install Angular Material | |||||
| - cd futbase | |||||
| - ng add @angular/material | |||||
| ## Generate Dummy data | |||||
| - cd futbase | |||||
| - npm i @openapitools/openapi-generator-cli -D | |||||
| - package.json: Scripts block: | |||||
| - "generate:api": "openapi-generator-cli generate -i ./openapi.yaml -g typescript-angular -o src/app/core/api/v1 -p=removeOperationIdPrefix=true" | |||||
| - Java must be installed | |||||
| - cd futbase | |||||
| ## Generate services from openapi.yaml | |||||
| - run sh generateApi.sh | |||||
| - (npm run generate:api | |||||
| - Wenn es nicht geht: brew install java | |||||
| - sudo ln -sfn /opt/homebrew/opt/openjdk/libexec/openjdk.jdk /Library/Java/JavaVirtualMachines/openjdk.jdk | |||||
| - java -version) | |||||
| - ACHTUNG: In Datei src/app/core/api/v1/model/partnerJsonId.ts diese zwei Zeilen löschen: | |||||
| - readonly type?: string; | |||||
| ## Module anlegen | |||||
| - cd app | |||||
| - ng g m registration --route register --module app.module | |||||
| ## Interesting Links | |||||
| - https://openapi-generator.tech/docs/installation | |||||
| - https://www.kevinboosten.dev/how-i-use-an-openapi-spec-in-my-angular-projects | |||||
| - https://material.angular.io/ | |||||
| - https://ng-bootstrap.github.io/#/home | |||||
| - https://medium.com/ngconf/new-input-binding-for-ngcomponentoutlet-cb18a86a739d | |||||
| - https://ng-bootstrap.github.io/#/components/typeahead/examples | |||||
| ## Install for autogeneration of forms: | |||||
| - brew install jq | |||||
| @@ -0,0 +1,137 @@ | |||||
| { | |||||
| "$schema": "./node_modules/@angular/cli/lib/config/schema.json", | |||||
| "version": 1, | |||||
| "newProjectRoot": "projects", | |||||
| "projects": { | |||||
| "futbase": { | |||||
| "projectType": "application", | |||||
| "schematics": { | |||||
| "@schematics/angular:component": { | |||||
| "style": "scss", | |||||
| "standalone": false | |||||
| }, | |||||
| "@schematics/angular:directive": { | |||||
| "standalone": false | |||||
| }, | |||||
| "@schematics/angular:pipe": { | |||||
| "standalone": false | |||||
| } | |||||
| }, | |||||
| "root": "", | |||||
| "sourceRoot": "src", | |||||
| "prefix": "app", | |||||
| "architect": { | |||||
| "build": { | |||||
| "builder": "@angular-devkit/build-angular:application", | |||||
| "options": { | |||||
| "outputPath": "../httpdocs/public/client", | |||||
| "baseHref": "/", | |||||
| "index": "src/index.html", | |||||
| "browser": "src/main.ts", | |||||
| "polyfills": [ | |||||
| "zone.js" | |||||
| ], | |||||
| "tsConfig": "tsconfig.app.json", | |||||
| "inlineStyleLanguage": "scss", | |||||
| "assets": [ | |||||
| "src/favicon.ico", | |||||
| "src/assets" | |||||
| ], | |||||
| "styles": [ | |||||
| "@angular/material/prebuilt-themes/indigo-pink.css", | |||||
| "node_modules/bootstrap/scss/bootstrap.scss", | |||||
| "node_modules/bootstrap-icons/font/bootstrap-icons.css", | |||||
| "src/styles.scss" | |||||
| ], | |||||
| "scripts": [ | |||||
| "node_modules/bootstrap/dist/js/bootstrap.bundle.min.js" | |||||
| ] | |||||
| }, | |||||
| "configurations": { | |||||
| "production": { | |||||
| "baseHref": "/client/", | |||||
| "budgets": [ | |||||
| { | |||||
| "type": "initial", | |||||
| "maximumWarning": "1mb", | |||||
| "maximumError": "2mb" | |||||
| }, | |||||
| { | |||||
| "type": "anyComponentStyle", | |||||
| "maximumWarning": "2kb", | |||||
| "maximumError": "4kb" | |||||
| } | |||||
| ], | |||||
| "outputHashing": "all" | |||||
| }, | |||||
| "development": { | |||||
| "optimization": false, | |||||
| "extractLicenses": false, | |||||
| "sourceMap": true, | |||||
| "fileReplacements": [ | |||||
| { | |||||
| "replace": "src/environments/environment.ts", | |||||
| "with": "src/environments/environment.development.ts" | |||||
| } | |||||
| ] | |||||
| }, | |||||
| "beta": { | |||||
| "baseHref": "/client/", | |||||
| "optimization": false, | |||||
| "extractLicenses": false, | |||||
| "sourceMap": true, | |||||
| "fileReplacements": [ | |||||
| { | |||||
| "replace": "src/environments/environment.ts", | |||||
| "with": "src/environments/environment.beta.ts" | |||||
| } | |||||
| ] | |||||
| } | |||||
| }, | |||||
| "defaultConfiguration": "production" | |||||
| }, | |||||
| "serve": { | |||||
| "builder": "@angular-devkit/build-angular:dev-server", | |||||
| "configurations": { | |||||
| "production": { | |||||
| "buildTarget": "futbase:build:production" | |||||
| }, | |||||
| "development": { | |||||
| "buildTarget": "futbase:build:development" | |||||
| }, | |||||
| "beta": { | |||||
| "buildTarget": "futbase:build:beta" | |||||
| } | |||||
| }, | |||||
| "defaultConfiguration": "development" | |||||
| }, | |||||
| "extract-i18n": { | |||||
| "builder": "@angular-devkit/build-angular:extract-i18n", | |||||
| "options": { | |||||
| "buildTarget": "futbase: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": [ | |||||
| "@angular/material/prebuilt-themes/indigo-pink.css", | |||||
| "src/styles.scss" | |||||
| ], | |||||
| "scripts": [] | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,34 @@ | |||||
| #!/bin/bash | |||||
| rm -rf ../httpdocs/public/client/* | |||||
| # Run ng build | |||||
| ng build | |||||
| # Check if ng build was successful | |||||
| if [ $? -ne 0 ]; then | |||||
| echo "ng build failed. Exiting script." | |||||
| exit 1 | |||||
| fi | |||||
| # Move files from ../httpdocs/public/client/browser to ../httpdocs/public/client | |||||
| mv ../httpdocs/public/client/browser/* ../httpdocs/public/client/ | |||||
| # Check if move was successful | |||||
| if [ $? -ne 0 ]; then | |||||
| echo "Failed to move files. Exiting script." | |||||
| exit 1 | |||||
| fi | |||||
| # Remove the browser folder | |||||
| rm -rf ../httpdocs/public/client/browser | |||||
| # Check if removal was successful | |||||
| if [ $? -ne 0 ]; then | |||||
| echo "Failed to remove browser folder. Exiting script." | |||||
| exit 1 | |||||
| fi | |||||
| echo "Build completed and files moved successfully." | |||||
| @@ -0,0 +1,23 @@ | |||||
| npm run generate:api | |||||
| find ./src/app/core/api/v1/model -type f -exec sed -i '' -e '' -e "s/hydramember/'hydra:member'/g" {} + | |||||
| find ./src/app/core/api/v1/model -type f -exec sed -i '' -e '' -e "s/hydratotalItems/'hydra:totalItems'/g" {} + | |||||
| find ./src/app/core/api/v1/model -type f -exec sed -i '' -e "s/hydraview/'hydra:view'/g" {} + | |||||
| find ./src/app/core/api/v1/model -type f -exec sed -i '' -e "s/hydrasearch/'hydra:search'/g" {} + | |||||
| find ./src/app/core/api/v1/model -type f -exec sed -i '' -e "s/hydratemplate/'hydra:template'/g" {} + | |||||
| find ./src/app/core/api/v1/model -type f -exec sed -i '' -e "s/hydravariableRepresentation/'hydra:variableRepresentation'/g" {} + | |||||
| find ./src/app/core/api/v1/model -type f -exec sed -i '' -e "s/hydramapping/'hydra:mapping'/g" {} + | |||||
| find ./src/app/core/api/v1/model -type f -exec sed -i '' -e "s/hydrafirst/'hydra:first'/g" {} + | |||||
| find ./src/app/core/api/v1/model -type f -exec sed -i '' -e "s/hydralast/'hydra:last'/g" {} + | |||||
| find ./src/app/core/api/v1/model -type f -exec sed -i '' -e "s/hydranext/'hydra:next'/g" {} + | |||||
| find ./src/app/core/api/v1/model -type f -exec sed -i '' -e "s/hydraprevious/'hydra:previous'/g" {} + | |||||
| # https://dev.to/martinmcwhorter/generate-angular-reactiveforms-from-swagger-openapi-35h9 -> alternative | |||||
| # https://github.com/Humbertda/ngx-openapi-form-generator -> alternative | |||||
| # https://github.com/verizonconnect/ngx-form-generator -> we use this one | |||||
| cat openapi.json | jq 'walk(if type == "object" then with_entries(select(.key | test("^@") | not)) else . end)' > openapi_no_hydra.json | |||||
| npx ngx-form-generator -i openapi_no_hydra.json -o src/app/_forms/ -f apiForms.ts | |||||
| rm openapi_no_hydra.json | |||||
| @@ -0,0 +1,7 @@ | |||||
| { | |||||
| "$schema": "./node_modules/@openapitools/openapi-generator-cli/config.schema.json", | |||||
| "spaces": 2, | |||||
| "generator-cli": { | |||||
| "version": "7.3.0" | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,56 @@ | |||||
| { | |||||
| "name": "futbase", | |||||
| "version": "0.0.0", | |||||
| "scripts": { | |||||
| "ng": "ng", | |||||
| "start": "ng serve", | |||||
| "build": "ng build", | |||||
| "watch": "ng build --watch --configuration development", | |||||
| "test": "ng test", | |||||
| "generate:api": "openapi-generator-cli generate -i ./openapi.yaml -g typescript-angular -o src/app/core/api/v1 -p=removeOperationIdPrefix=true" | |||||
| }, | |||||
| "private": true, | |||||
| "dependencies": { | |||||
| "@angular/animations": "^17.0.0", | |||||
| "@angular/cdk": "^17.0.4", | |||||
| "@angular/common": "^17.0.0", | |||||
| "@angular/compiler": "^17.0.0", | |||||
| "@angular/core": "^17.0.0", | |||||
| "@angular/forms": "^17.0.0", | |||||
| "@angular/material": "^17.0.4", | |||||
| "@angular/platform-browser": "^17.0.0", | |||||
| "@angular/platform-browser-dynamic": "^17.0.0", | |||||
| "@angular/router": "^17.0.0", | |||||
| "@ng-bootstrap/ng-bootstrap": "^16.0.0-rc.2", | |||||
| "@ngx-translate/core": "^15.0.0", | |||||
| "@ngx-translate/http-loader": "^8.0.0", | |||||
| "@popperjs/core": "^2.11.8", | |||||
| "@types/node": "^20.11.5", | |||||
| "bootstrap": "^5.3.2", | |||||
| "bootstrap-icons": "^1.11.2", | |||||
| "rxjs": "~7.8.0", | |||||
| "tslib": "^2.3.0", | |||||
| "uuid": "^9.0.1", | |||||
| "zone.js": "~0.14.2" | |||||
| }, | |||||
| "devDependencies": { | |||||
| "@angular-devkit/build-angular": "^17.0.7", | |||||
| "@angular/cli": "^17.0.7", | |||||
| "@angular/compiler-cli": "^17.0.0", | |||||
| "@openapitools/openapi-generator-cli": "^2.7.0", | |||||
| "@types/jasmine": "~5.1.0", | |||||
| "@types/uuid": "^9.0.8", | |||||
| "@verizonconnect/ngx-form-generator": "^1.2.0", | |||||
| "jasmine-core": "~5.1.0", | |||||
| "karma": "~6.4.0", | |||||
| "karma-chrome-launcher": "~3.2.0", | |||||
| "karma-coverage": "~2.2.0", | |||||
| "karma-jasmine": "~5.1.0", | |||||
| "karma-jasmine-html-reporter": "~2.1.0", | |||||
| "typescript": "~5.2.2" | |||||
| }, | |||||
| "description": "This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 17.0.7.", | |||||
| "main": "index.js", | |||||
| "author": "", | |||||
| "license": "ISC" | |||||
| } | |||||
| @@ -0,0 +1,7 @@ | |||||
| <div class="alert-box" *ngIf="alerts && alerts.length > 0"> | |||||
| <div *ngFor="let alert of alerts" class="{{cssClass(alert)}}"> | |||||
| <span [innerHTML]="alert.message"></span> | |||||
| <button class="btn-close" (click)="removeAlert(alert)"></button> | |||||
| </div> | |||||
| <div (click)="removeAllAlerts()" class="alert alerts-remove">Click here to remove all</div> | |||||
| </div> | |||||
| @@ -0,0 +1,100 @@ | |||||
| import { Component, OnInit, OnDestroy, Input } from '@angular/core'; | |||||
| import { Router, NavigationStart } from '@angular/router'; | |||||
| import { Subscription } from 'rxjs'; | |||||
| import { Alert, AlertType } from '@app/_models'; | |||||
| import { AlertService } from '@app/_services'; | |||||
| @Component({ selector: 'alert', templateUrl: 'alert.component.html' }) | |||||
| export class AlertComponent implements OnInit, OnDestroy { | |||||
| @Input() id = 'default-alert'; | |||||
| @Input() fade = true; | |||||
| alerts: Alert[] = []; | |||||
| alertSubscription!: Subscription; | |||||
| routeSubscription!: Subscription; | |||||
| constructor(private router: Router, private alertService: AlertService) { } | |||||
| ngOnInit() { | |||||
| // subscribe to new alert notifications | |||||
| this.alertSubscription = this.alertService.onAlert(this.id) | |||||
| .subscribe(alert => { | |||||
| // clear alerts when an empty alert is received | |||||
| if (!alert.message) { | |||||
| // filter out alerts without 'keepAfterRouteChange' flag | |||||
| this.alerts = this.alerts.filter(x => x.keepAfterRouteChange); | |||||
| // remove 'keepAfterRouteChange' flag on the rest | |||||
| this.alerts.forEach(x => delete x.keepAfterRouteChange); | |||||
| return; | |||||
| } | |||||
| // add alert to array | |||||
| this.alerts.push(alert); | |||||
| // auto close alert if required | |||||
| if (alert.autoClose) { | |||||
| setTimeout(() => this.removeAlert(alert), 1000); | |||||
| } | |||||
| }); | |||||
| // clear alerts on location change | |||||
| this.routeSubscription = this.router.events.subscribe(event => { | |||||
| if (event instanceof NavigationStart) { | |||||
| this.alertService.clear(this.id); | |||||
| } | |||||
| }); | |||||
| } | |||||
| ngOnDestroy() { | |||||
| // unsubscribe to avoid memory leaks | |||||
| this.alertSubscription.unsubscribe(); | |||||
| this.routeSubscription.unsubscribe(); | |||||
| } | |||||
| removeAlert(alert: Alert) { | |||||
| // check if already removed to prevent error on auto close | |||||
| if (!this.alerts.includes(alert)) return; | |||||
| if (this.fade) { | |||||
| // fade out alert | |||||
| alert.fade = true; | |||||
| // remove alert after faded out | |||||
| setTimeout(() => { | |||||
| this.alerts = this.alerts.filter(x => x !== alert); | |||||
| }, 250); | |||||
| } else { | |||||
| // remove alert | |||||
| this.alerts = this.alerts.filter(x => x !== alert); | |||||
| } | |||||
| } | |||||
| removeAllAlerts() { | |||||
| this.alerts.forEach((alert) => this.removeAlert(alert)); | |||||
| } | |||||
| cssClass(alert: Alert) { | |||||
| if (!alert) return; | |||||
| const classes = ['alert', 'alert-dismissible', 'mb-2', 'container']; | |||||
| const alertTypeClass = { | |||||
| [AlertType.Success]: 'alert-success', | |||||
| [AlertType.Error]: 'alert-danger', | |||||
| [AlertType.Info]: 'alert-info', | |||||
| [AlertType.Warning]: 'alert-warning' | |||||
| } | |||||
| if (alert.type !== undefined) { | |||||
| classes.push(alertTypeClass[alert.type]); | |||||
| } | |||||
| if (alert.fade) { | |||||
| classes.push('fade'); | |||||
| } | |||||
| return classes.join(' '); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,10 @@ | |||||
| <div [formGroup]="form" class="row"> | |||||
| <div class="col-6"> | |||||
| <label for="{{ inputId }}-date">{{ label }} ({{ 'basic.date' | translate }}):</label> | |||||
| <input type="date" id="{{ inputId }}-date" class="form-control" formControlName="date" [readonly]="readonly"> | |||||
| </div> | |||||
| <div class="col-6"> | |||||
| <label for="{{ inputId }}-time">{{ label }} ({{ 'basic.time' | translate }}):</label> | |||||
| <input type="time" id="{{ inputId }}-time" class="form-control" formControlName="time" step="1" [readonly]="readonly"> | |||||
| </div> | |||||
| </div> | |||||
| @@ -0,0 +1,23 @@ | |||||
| import { ComponentFixture, TestBed } from '@angular/core/testing'; | |||||
| import { DatetimePickerComponent } from './datetime-picker.component'; | |||||
| describe('DatetimePickerComponent', () => { | |||||
| let component: DatetimePickerComponent; | |||||
| let fixture: ComponentFixture<DatetimePickerComponent>; | |||||
| beforeEach(async () => { | |||||
| await TestBed.configureTestingModule({ | |||||
| declarations: [DatetimePickerComponent] | |||||
| }) | |||||
| .compileComponents(); | |||||
| fixture = TestBed.createComponent(DatetimePickerComponent); | |||||
| component = fixture.componentInstance; | |||||
| fixture.detectChanges(); | |||||
| }); | |||||
| it('should create', () => { | |||||
| expect(component).toBeTruthy(); | |||||
| }); | |||||
| }); | |||||
| @@ -0,0 +1,70 @@ | |||||
| import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core'; | |||||
| import {FormBuilder, FormGroup} from "@angular/forms"; | |||||
| @Component({ | |||||
| selector: 'app-datetime-picker', | |||||
| templateUrl: './datetime-picker.component.html', | |||||
| styleUrl: './datetime-picker.component.scss' | |||||
| }) | |||||
| export class DatetimePickerComponent implements OnInit { | |||||
| @Input() label: string = 'Date and Time'; | |||||
| @Input() inputId: string = 'myId'; | |||||
| @Input() initialValue: string | null = null; | |||||
| @Input() readonly: boolean = false; | |||||
| @Output() dateTimeChange = new EventEmitter<string | null>(); | |||||
| form: FormGroup; | |||||
| constructor(private fb: FormBuilder) { | |||||
| this.form = this.fb.group({ | |||||
| date: [''], | |||||
| time: [''] | |||||
| }); | |||||
| } | |||||
| ngOnInit() { | |||||
| if (this.initialValue) { | |||||
| const date = new Date(this.initialValue); | |||||
| this.form.patchValue({ | |||||
| date: this.formatDate(date), | |||||
| time: this.formatTime(date) | |||||
| }); | |||||
| } | |||||
| if (this.readonly) { | |||||
| this.form.disable(); | |||||
| } | |||||
| this.form.valueChanges.subscribe(() => { | |||||
| if (!this.readonly) { | |||||
| this.emitDateTime(); | |||||
| } | |||||
| }); | |||||
| } | |||||
| private formatDate(date: Date): string { | |||||
| return date.toLocaleDateString('en-CA'); | |||||
| } | |||||
| private formatTime(date: Date): string { | |||||
| return date.toLocaleTimeString('en-GB', { hour12: false }); | |||||
| } | |||||
| private emitDateTime() { | |||||
| const { date, time } = this.form.value; | |||||
| if (date && time) { | |||||
| const [year, month, day] = date.split('-'); | |||||
| const [hours, minutes, seconds] = time.split(':'); | |||||
| const dateTime = new Date(Number(year), Number(month) - 1, Number(day), Number(hours), Number(minutes), Number(seconds)); | |||||
| // Format the date to match the loaded format | |||||
| const formattedDate = dateTime.toLocaleString('sv-SE', { timeZone: 'Europe/Berlin' }).replace(' ', 'T') + '+02:00'; | |||||
| console.log('Emitting datetime:', formattedDate); | |||||
| this.dateTimeChange.emit(formattedDate); | |||||
| } else { | |||||
| console.log('Emitting null datetime'); | |||||
| this.dateTimeChange.emit(null); | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,141 @@ | |||||
| <div class="spt-form"> | |||||
| <div class="row"> | |||||
| <ng-container *ngFor="let filter of filterStates"> | |||||
| <ng-container [ngSwitch]="filter.type"> | |||||
| <!-- Boolean Filter --> | |||||
| <ng-container *ngSwitchCase="FILTER_TYPE_BOOLEAN"> | |||||
| <div class="col-12 col-sm-6 col-md-4 col-lg-3 mb-3 switch-widget"> | |||||
| <div class="d-flex align-items-center mb-1"> | |||||
| <input type="checkbox" | |||||
| class="mini-check" | |||||
| [checked]="filter.active" | |||||
| (change)="onFilterActiveChanged(filter.field, $event, filter.subResource)"> | |||||
| <p class="form-label">{{ getColumnText(filter.field) | translate }}</p> | |||||
| </div> | |||||
| <label class="switch"> | |||||
| <input type="checkbox" | |||||
| [checked]="filter.value" | |||||
| (change)="onBooleanFilterChanged(filter.field, $event, filter.subResource)"> | |||||
| <span class="slider round"></span> | |||||
| </label> | |||||
| </div> | |||||
| </ng-container> | |||||
| <!-- Text Filter --> | |||||
| <ng-container *ngSwitchCase="FILTER_TYPE_TEXT"> | |||||
| <div class="col-12 col-sm-6 col-md-4 col-lg-3 mb-3"> | |||||
| <div class="d-flex align-items-center mb-1"> | |||||
| <input type="checkbox" | |||||
| class="mini-check" | |||||
| [checked]="filter.active" | |||||
| (change)="onFilterActiveChanged(filter.field, $event, filter.subResource)"> | |||||
| <label class="form-label">{{ getColumnText(filter.field) | translate }}</label> | |||||
| </div> | |||||
| <input type="text" | |||||
| class="form-control" | |||||
| [value]="filter.value" | |||||
| (input)="onTextFilterChanged(filter.field, $event, filter.subResource)"> | |||||
| </div> | |||||
| </ng-container> | |||||
| <!-- Date Filter --> | |||||
| <ng-container *ngSwitchCase="FILTER_TYPE_DATE"> | |||||
| <div class="col-12 col-sm-6 col-md-4 col-lg-3 mb-3"> | |||||
| <div class="d-flex align-items-center mb-1"> | |||||
| <input type="checkbox" | |||||
| class="mini-check" | |||||
| [checked]="filter.active" | |||||
| (change)="onFilterActiveChanged(filter.field, $event, filter.subResource)"> | |||||
| <label class="form-label">{{ getColumnText(filter.field) | translate }}</label> | |||||
| </div> | |||||
| <div class="row"> | |||||
| <div class="col-6"> | |||||
| <input type="date" | |||||
| class="form-control" | |||||
| [value]="filter.value.start" | |||||
| (change)="onDateFilterChanged(filter.field, 'start', $event, filter.subResource)"> | |||||
| </div> | |||||
| <div class="col-6"> | |||||
| <input type="date" | |||||
| class="form-control" | |||||
| [value]="filter.value.end" | |||||
| (change)="onDateFilterChanged(filter.field, 'end', $event, filter.subResource)"> | |||||
| </div> | |||||
| </div> | |||||
| </div> | |||||
| </ng-container> | |||||
| <!-- Number Filter --> | |||||
| <ng-container *ngSwitchCase="FILTER_TYPE_NUMBER"> | |||||
| <div class="col-12 col-sm-6 col-md-4 col-lg-3 mb-3"> | |||||
| <div class="d-flex align-items-center mb-1"> | |||||
| <input type="checkbox" | |||||
| class="mini-check" | |||||
| [checked]="filter.active" | |||||
| (change)="onFilterActiveChanged(filter.field, $event, filter.subResource)"> | |||||
| <label class="form-label">{{ getColumnText(filter.field) | translate }}</label> | |||||
| </div> | |||||
| <div class="row"> | |||||
| <div class="col-6"> | |||||
| <input type="number" | |||||
| class="form-control" | |||||
| [value]="filter.value.min" | |||||
| (input)="onNumberFilterChanged(filter.field, 'min', $event, filter.subResource)" | |||||
| placeholder="Min"> | |||||
| </div> | |||||
| <div class="col-6"> | |||||
| <input type="number" | |||||
| class="form-control" | |||||
| [value]="filter.value.max" | |||||
| (input)="onNumberFilterChanged(filter.field, 'max', $event, filter.subResource)" | |||||
| placeholder="Max"> | |||||
| </div> | |||||
| </div> | |||||
| </div> | |||||
| </ng-container> | |||||
| <ng-container *ngSwitchCase="FILTER_TYPE_CHECKBOXES"> | |||||
| <div class="col-12 mb-3 switch-widget"> | |||||
| <div class="d-flex align-items-center mb-1"> | |||||
| <input type="checkbox" | |||||
| class="mini-check" | |||||
| [checked]="filter.active" | |||||
| (change)="onFilterActiveChanged(filter.field, $event, filter.subResource)"> | |||||
| <p class="form-label">{{ getColumnText(filter.field) | translate }}</p> | |||||
| </div> | |||||
| <div class="row"> | |||||
| <div *ngFor="let option of filter.options" class="col-12 col-sm-6 col-md-3 col-lg-2 mb-3"> | |||||
| <div class="d-flex align-items-center mb-1"> | |||||
| <p class="form-label">{{ option }}</p> | |||||
| </div> | |||||
| <label class="switch"> | |||||
| <input type="checkbox" | |||||
| [id]="filter.field + '_' + option" | |||||
| (change)="onCheckboxesFilterChanged(filter.field, option, $event, filter.subResource)" | |||||
| [checked]="filter.value[option]" | |||||
| class="form-check-input"> | |||||
| <span class="slider round"></span> | |||||
| </label> | |||||
| </div> | |||||
| </div> | |||||
| </div> | |||||
| <!-- <div class="col-12 col-sm-6 col-md-4 col-lg-3 mb-3">--> | |||||
| <!-- <input type="checkbox"--> | |||||
| <!-- [id]="filter.field + '_' + option"--> | |||||
| <!-- (change)="onCheckboxesFilterChanged(filter.field, option, $event, filter.subResource)"--> | |||||
| <!-- [checked]="filter.value[option]"--> | |||||
| <!-- class="form-check-input">--> | |||||
| <!-- <label [for]="filter.field + '_' + option" class="form-check-label">--> | |||||
| <!-- {{ option }}--> | |||||
| <!-- </label>--> | |||||
| <!-- </div>--> | |||||
| </ng-container> | |||||
| </ng-container> | |||||
| </ng-container> | |||||
| </div> | |||||
| <div class="row mb-3"> | |||||
| <div class="col-12 d-flex justify-content-end"> | |||||
| <button (click)="saveFilterConfig()" class="btn btn-primary me-3">{{ 'basic.saveSettings' | translate }}</button> | |||||
| <button (click)="resetAllFilters()" class="btn btn-primary">{{ 'basic.resetFilters' | translate }}</button> | |||||
| </div> | |||||
| </div> | |||||
| </div> | |||||
| @@ -0,0 +1,23 @@ | |||||
| import { ComponentFixture, TestBed } from '@angular/core/testing'; | |||||
| import { FilterBarComponent } from './filter-bar.component'; | |||||
| describe('FilterBarComponent', () => { | |||||
| let component: FilterBarComponent; | |||||
| let fixture: ComponentFixture<FilterBarComponent>; | |||||
| beforeEach(async () => { | |||||
| await TestBed.configureTestingModule({ | |||||
| declarations: [FilterBarComponent] | |||||
| }) | |||||
| .compileComponents(); | |||||
| fixture = TestBed.createComponent(FilterBarComponent); | |||||
| component = fixture.componentInstance; | |||||
| fixture.detectChanges(); | |||||
| }); | |||||
| it('should create', () => { | |||||
| expect(component).toBeTruthy(); | |||||
| }); | |||||
| }); | |||||
| @@ -0,0 +1,301 @@ | |||||
| import { Component, EventEmitter, Input, OnInit, OnDestroy, Output } from '@angular/core'; | |||||
| import { ListColDefinition } from "@app/_components/list/list-col-definition"; | |||||
| import { Subject, debounceTime, takeUntil } from 'rxjs'; | |||||
| import {filter} from "rxjs/operators"; | |||||
| interface FilterState { | |||||
| field: string; | |||||
| type: string; | |||||
| value: any; | |||||
| active: boolean; | |||||
| subResource?: string; | |||||
| options?: string[]; | |||||
| } | |||||
| interface NumberInput { | |||||
| field: string; | |||||
| boundType: 'min' | 'max'; | |||||
| value: number | null; | |||||
| subResource?: string; | |||||
| } | |||||
| @Component({ | |||||
| selector: 'app-filter-bar', | |||||
| templateUrl: './filter-bar.component.html', | |||||
| styleUrls: ['./filter-bar.component.scss'] | |||||
| }) | |||||
| export class FilterBarComponent implements OnInit, OnDestroy { | |||||
| @Input() public listColDefinitions!: ListColDefinition[]; | |||||
| @Input() public filterConfig!: string | null; | |||||
| @Output() public filterInit = new EventEmitter<{filters: Record<string, any>, activeCount: number}>(); | |||||
| @Output() public filterChanged = new EventEmitter<{filters: Record<string, any>, activeCount: number}>(); | |||||
| @Output() public filterSave = new EventEmitter(); | |||||
| public static readonly FILTER_TYPE_BOOLEAN: string = 'boolean'; | |||||
| public static readonly FILTER_TYPE_TEXT: string = 'text'; | |||||
| public static readonly FILTER_TYPE_DATE: string = 'date'; | |||||
| public static readonly FILTER_TYPE_NUMBER: string = 'number'; | |||||
| public static readonly FILTER_TYPE_CHECKBOXES: string = 'checkboxes'; | |||||
| public filterStates: FilterState[] = []; | |||||
| private numberInputSubject = new Subject<NumberInput>(); | |||||
| private textInputSubject = new Subject<{field: string, value: string, subResource?: string}>(); | |||||
| private destroy$ = new Subject<void>(); | |||||
| ngOnInit(): void { | |||||
| this.initializeFilterStates(); | |||||
| this.setupNumberInputDebounce(); | |||||
| this.setupTextInputDebounce(); | |||||
| } | |||||
| ngOnDestroy(): void { | |||||
| this.destroy$.next(); | |||||
| this.destroy$.complete(); | |||||
| } | |||||
| private countActiveFilters(): number { | |||||
| return this.filterStates.filter(state => state.active).length; | |||||
| } | |||||
| private initializeFilterStates(): void { | |||||
| let filterSettingsObj: { [key: string]: any } = {}; | |||||
| if (this.filterConfig !== null) { | |||||
| filterSettingsObj = JSON.parse(this.filterConfig); | |||||
| } | |||||
| this.filterStates = this.listColDefinitions | |||||
| .filter(col => col.filterType) | |||||
| .map(col => { | |||||
| let value = this.getInitialValueForType(col.filterType || ''); | |||||
| let active = false; | |||||
| if (col.field !== undefined && filterSettingsObj.hasOwnProperty(col.field)) { | |||||
| value = filterSettingsObj[col.field]; | |||||
| active = true; | |||||
| } | |||||
| const filterState: FilterState = { | |||||
| field: col.field || '', | |||||
| type: col.filterType || '', | |||||
| value: value, | |||||
| active: active, | |||||
| subResource: col.subResource, | |||||
| options: col.filterOptions | |||||
| }; | |||||
| return filterState; | |||||
| }); | |||||
| const activeFilters: Record<string, any> = this.getActiveFilters(); | |||||
| const activeCount = this.countActiveFilters(); | |||||
| this.filterInit.emit({ filters: activeFilters, activeCount }); | |||||
| } | |||||
| private setupNumberInputDebounce(): void { | |||||
| this.numberInputSubject.pipe( | |||||
| debounceTime(300), | |||||
| takeUntil(this.destroy$) | |||||
| ).subscribe(input => { | |||||
| this.updateNumberFilter(input.field, input.boundType, input.value, input.subResource); | |||||
| }); | |||||
| } | |||||
| private setupTextInputDebounce(): void { | |||||
| this.textInputSubject.pipe( | |||||
| debounceTime(300), | |||||
| takeUntil(this.destroy$) | |||||
| ).subscribe(({field, value, subResource}) => { | |||||
| this.updateTextFilter(field, value, subResource); | |||||
| }); | |||||
| } | |||||
| private getInitialValueForType(type: string, options?: string[]): any { | |||||
| switch (type) { | |||||
| case FilterBarComponent.FILTER_TYPE_BOOLEAN: | |||||
| return false; | |||||
| case FilterBarComponent.FILTER_TYPE_TEXT: | |||||
| return ''; | |||||
| case FilterBarComponent.FILTER_TYPE_DATE: | |||||
| return { start: null, end: null }; | |||||
| case FilterBarComponent.FILTER_TYPE_NUMBER: | |||||
| return { min: null, max: null }; | |||||
| case FilterBarComponent.FILTER_TYPE_CHECKBOXES: | |||||
| if (options) { | |||||
| return options.reduce((acc, option) => { | |||||
| acc[option] = false; | |||||
| return acc; | |||||
| }, {} as {[key: string]: boolean}); | |||||
| } | |||||
| return {}; | |||||
| default: | |||||
| return null; | |||||
| } | |||||
| } | |||||
| private findFilterState(field: string, subResource?: string): FilterState | undefined { | |||||
| return this.filterStates.find(state => | |||||
| state.field === field && state.subResource === subResource | |||||
| ); | |||||
| } | |||||
| onBooleanFilterChanged(field: string, event: Event, subResource?: string): void { | |||||
| const target = event.target as HTMLInputElement; | |||||
| this.onFilterChanged(field, target.checked, subResource); | |||||
| } | |||||
| onTextFilterChanged(field: string, event: Event, subResource?: string): void { | |||||
| const target = event.target as HTMLInputElement; | |||||
| this.textInputSubject.next({ field, value: target.value, subResource }); | |||||
| } | |||||
| private updateTextFilter(field: string, value: string, subResource?: string): void { | |||||
| const filterState = this.findFilterState(field, subResource); | |||||
| if (filterState && filterState.type === FilterBarComponent.FILTER_TYPE_TEXT) { | |||||
| filterState.value = value; | |||||
| this.emitActiveFilters(); | |||||
| } | |||||
| } | |||||
| onDateFilterChanged(field: string, dateType: 'start' | 'end', event: Event, subResource?: string): void { | |||||
| const target = event.target as HTMLInputElement; | |||||
| const filterState = this.findFilterState(field, subResource); | |||||
| if (filterState && filterState.type === FilterBarComponent.FILTER_TYPE_DATE) { | |||||
| filterState.value = { ...filterState.value, [dateType]: target.value }; | |||||
| this.emitActiveFilters(); | |||||
| } | |||||
| } | |||||
| onNumberFilterChanged(field: string, boundType: 'min' | 'max', event: Event, subResource?: string): void { | |||||
| const target = event.target as HTMLInputElement; | |||||
| const numValue = target.value === '' ? null : Number(target.value); | |||||
| this.numberInputSubject.next({ field, boundType, value: numValue, subResource }); | |||||
| } | |||||
| onCheckboxesFilterChanged(field: string, option: string, event: Event, subResource?: string): void { | |||||
| const target = event.target as HTMLInputElement; | |||||
| const filterState = this.findFilterState(field, subResource); | |||||
| console.log(filterState); | |||||
| if (filterState && filterState.type === FilterBarComponent.FILTER_TYPE_CHECKBOXES) { | |||||
| filterState.value[option] = target.checked; | |||||
| console.log('jojoj'); | |||||
| this.emitActiveFilters(); | |||||
| } | |||||
| } | |||||
| private updateNumberFilter(field: string, boundType: 'min' | 'max', value: number | null, subResource?: string): void { | |||||
| const filterState = this.findFilterState(field, subResource); | |||||
| if (filterState && filterState.type === FilterBarComponent.FILTER_TYPE_NUMBER) { | |||||
| filterState.value = { ...filterState.value, [boundType]: value }; | |||||
| this.emitActiveFilters(); | |||||
| } | |||||
| } | |||||
| onFilterChanged(field: string, value: any, subResource?: string): void { | |||||
| const filterState = this.findFilterState(field, subResource); | |||||
| if (filterState) { | |||||
| filterState.value = value; | |||||
| this.emitActiveFilters(); | |||||
| } | |||||
| } | |||||
| onFilterActiveChanged(field: string, event: Event, subResource?: string): void { | |||||
| const target = event.target as HTMLInputElement; | |||||
| const filterState = this.findFilterState(field, subResource); | |||||
| if (filterState) { | |||||
| filterState.active = target.checked; | |||||
| if (filterState.type === FilterBarComponent.FILTER_TYPE_CHECKBOXES) { | |||||
| // Reset all checkbox values when the filter is deactivated | |||||
| if (!filterState.active) { | |||||
| for (const option in filterState.value) { | |||||
| filterState.value[option] = false; | |||||
| } | |||||
| } | |||||
| } | |||||
| this.emitActiveFilters(); | |||||
| } | |||||
| } | |||||
| resetAllFilters(): void { | |||||
| this.filterStates.forEach(state => { | |||||
| state.active = false; | |||||
| state.value = this.getInitialValueForType(state.type); | |||||
| }); | |||||
| this.emitActiveFilters(); | |||||
| } | |||||
| private getActiveFilters(): Record<string, any> { | |||||
| const activeFilters: Record<string, any> = {}; | |||||
| this.filterStates | |||||
| .filter(state => state.active) | |||||
| .forEach(state => { | |||||
| if (state.subResource) { | |||||
| if (!activeFilters[state.subResource]) { | |||||
| activeFilters[state.subResource] = {}; | |||||
| } | |||||
| this.setFilterValue(activeFilters[state.subResource], state); | |||||
| } else { | |||||
| this.setFilterValue(activeFilters, state); | |||||
| } | |||||
| }); | |||||
| return activeFilters; | |||||
| } | |||||
| private emitActiveFilters(): void { | |||||
| const activeFilters: Record<string, any> = this.getActiveFilters(); | |||||
| const activeCount = this.countActiveFilters(); | |||||
| this.filterChanged.emit({ filters: activeFilters, activeCount }); | |||||
| } | |||||
| private setFilterValue(obj: Record<string, any>, state: FilterState): void { | |||||
| if (state.type === FilterBarComponent.FILTER_TYPE_BOOLEAN) { | |||||
| obj[state.field] = state.value; | |||||
| } else if (state.type === FilterBarComponent.FILTER_TYPE_TEXT) { | |||||
| obj[state.field] = state.value; | |||||
| } else if (state.type === FilterBarComponent.FILTER_TYPE_DATE) { | |||||
| if (state.value.start !== null || state.value.end !== null) { | |||||
| obj[state.field] = state.value; | |||||
| } else { | |||||
| obj[state.field] = {}; | |||||
| } | |||||
| } else if (state.type === FilterBarComponent.FILTER_TYPE_NUMBER) { | |||||
| if (state.value.min !== null || state.value.max !== null) { | |||||
| obj[state.field] = state.value; | |||||
| } else { | |||||
| obj[state.field] = {}; | |||||
| } | |||||
| } else if (state.type === FilterBarComponent.FILTER_TYPE_CHECKBOXES) { | |||||
| // Add this block to handle checkbox filters | |||||
| const selectedOptions = Object.entries(state.value) | |||||
| .filter(([_, isChecked]) => isChecked) | |||||
| .map(([option, _]) => option); | |||||
| if (selectedOptions.length > 0) { | |||||
| obj[state.field] = selectedOptions; | |||||
| } | |||||
| } | |||||
| } | |||||
| saveFilterConfig(): void { | |||||
| this.filterSave.emit(); | |||||
| } | |||||
| getColumnText(field: string): string { | |||||
| const column = this.listColDefinitions.find(col => col.field === field); | |||||
| return column ? column.text : ''; | |||||
| } | |||||
| get FILTER_TYPE_BOOLEAN(): string { | |||||
| return FilterBarComponent.FILTER_TYPE_BOOLEAN; | |||||
| } | |||||
| get FILTER_TYPE_TEXT(): string { | |||||
| return FilterBarComponent.FILTER_TYPE_TEXT; | |||||
| } | |||||
| get FILTER_TYPE_DATE(): string { | |||||
| return FilterBarComponent.FILTER_TYPE_DATE; | |||||
| } | |||||
| get FILTER_TYPE_NUMBER(): string { | |||||
| return FilterBarComponent.FILTER_TYPE_NUMBER; | |||||
| } | |||||
| get FILTER_TYPE_CHECKBOXES(): string { | |||||
| return FilterBarComponent.FILTER_TYPE_CHECKBOXES; | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1 @@ | |||||
| export * from './alert.component'; | |||||
| @@ -0,0 +1,83 @@ | |||||
| <div class="row ps-2 position-relative"> | |||||
| <div id="btn-sidebar" (click)="toggleSidebar()" [class.nav-open]="navOpen"></div> | |||||
| <div class="spt-sidebar" [class.nav-open]="navOpen"> | |||||
| <ul class="nav flex-column"> | |||||
| <li class="nav-item mb-3"> | |||||
| <a class="card" routerLink="/dashboard" routerLinkActive="active"> | |||||
| <div class="card-body position-relative" data-cat="dashboard"> | |||||
| <h3 class="position-absolute m-0">{{'dashboard.view' | translate}}</h3> | |||||
| </div> | |||||
| </a> | |||||
| </li> | |||||
| <li class="nav-item mb-3"> | |||||
| <a class="card" routerLink="/system" routerLinkActive="active"> | |||||
| <div class="card-body position-relative" data-cat="system"> | |||||
| <h3 class="position-absolute m-0">{{'system.view' | translate}}</h3> | |||||
| </div> | |||||
| </a> | |||||
| </li> | |||||
| <li class="nav-item mb-3"> | |||||
| <a class="card" routerLink="/logs" routerLinkActive="active"> | |||||
| <div class="card-body position-relative" data-cat="logs"> | |||||
| <h3 class="position-absolute m-0">{{'logs.view' | translate}}</h3> | |||||
| </div> | |||||
| </a> | |||||
| </li> | |||||
| <li class="nav-item mb-3"> | |||||
| <a class="card" routerLink="/game-accounts" routerLinkActive="active"> | |||||
| <div class="card-body position-relative" data-cat="account"> | |||||
| <h3 class="position-absolute m-0">{{'game_account.view' | translate}}</h3> | |||||
| </div> | |||||
| </a> | |||||
| </li> | |||||
| <li class="nav-item mb-3"> | |||||
| <a class="card" routerLink="/candidates" routerLinkActive="active"> | |||||
| <div class="card-body position-relative" data-cat="candidates"> | |||||
| <h3 class="position-absolute m-0">{{'candidate.view' | translate}}</h3> | |||||
| </div> | |||||
| </a> | |||||
| </li> | |||||
| <li class="nav-item mb-3"> | |||||
| <a class="card" routerLink="/tradepile-items" routerLinkActive="active"> | |||||
| <div class="card-body position-relative" data-cat="tradepile-items"> | |||||
| <h3 class="position-absolute m-0">{{'tradepile_item.view' | translate}}</h3> | |||||
| </div> | |||||
| </a> | |||||
| </li> | |||||
| <li class="nav-item mb-3"> | |||||
| <a class="card" routerLink="/sales" routerLinkActive="active"> | |||||
| <div class="card-body position-relative" data-cat="sales"> | |||||
| <h3 class="position-absolute m-0">{{'sales.view' | translate}}</h3> | |||||
| </div> | |||||
| </a> | |||||
| </li> | |||||
| <li class="nav-item mb-3"> | |||||
| <a class="card" routerLink="/sniping" routerLinkActive="active"> | |||||
| <div class="card-body position-relative" data-cat="sniping"> | |||||
| <h3 class="position-absolute m-0">{{'sniping.view' | translate}}</h3> | |||||
| </div> | |||||
| </a> | |||||
| </li> | |||||
| <li class="nav-item mb-3"> | |||||
| <a class="card" routerLink="/profiles" routerLinkActive="active"> | |||||
| <div class="card-body position-relative" data-cat="profile"> | |||||
| <h3 class="position-absolute m-0">{{'profile.view' | translate}}</h3> | |||||
| </div> | |||||
| </a> | |||||
| </li> | |||||
| <li class="nav-item mb-3"> | |||||
| <a class="card" routerLink="/users" routerLinkActive="active"> | |||||
| <div class="card-body position-relative" data-cat="user"> | |||||
| <h3 class="position-absolute m-0">{{'basic.users' | translate}}</h3> | |||||
| </div> | |||||
| </a> | |||||
| </li> | |||||
| </ul> | |||||
| </div> | |||||
| <div class="pb-5 spt-main"> | |||||
| <div class="pe-3 pt-3"> | |||||
| <div class="btn btn-secondary mb-1" (click)="goBack()" id="go-back">< {{'basic.back' | translate}}</div> | |||||
| <router-outlet></router-outlet> | |||||
| </div> | |||||
| </div> | |||||
| </div> | |||||
| @@ -0,0 +1,23 @@ | |||||
| import { ComponentFixture, TestBed } from '@angular/core/testing'; | |||||
| import { TwoColumnComponent } from './two-column.component'; | |||||
| describe('TwoColumnComponent', () => { | |||||
| let component: TwoColumnComponent; | |||||
| let fixture: ComponentFixture<TwoColumnComponent>; | |||||
| beforeEach(async () => { | |||||
| await TestBed.configureTestingModule({ | |||||
| declarations: [TwoColumnComponent] | |||||
| }) | |||||
| .compileComponents(); | |||||
| fixture = TestBed.createComponent(TwoColumnComponent); | |||||
| component = fixture.componentInstance; | |||||
| fixture.detectChanges(); | |||||
| }); | |||||
| it('should create', () => { | |||||
| expect(component).toBeTruthy(); | |||||
| }); | |||||
| }); | |||||
| @@ -0,0 +1,26 @@ | |||||
| import {Component} from '@angular/core'; | |||||
| import {Location} from "@angular/common"; | |||||
| @Component({ | |||||
| selector: 'app-two-column', | |||||
| templateUrl: './two-column.component.html', | |||||
| styleUrl: './two-column.component.scss' | |||||
| }) | |||||
| export class TwoColumnComponent { | |||||
| public navOpen: boolean; | |||||
| constructor(private _location: Location) { | |||||
| this.navOpen = false; | |||||
| } | |||||
| goBack() { | |||||
| this._location.back(); | |||||
| } | |||||
| toggleSidebar() { | |||||
| this.navOpen = !this.navOpen; | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,4 @@ | |||||
| <span *ngIf="user"> | |||||
| <img src="./assets/images/icons/user.svg" class="icon-mini" alt=""/><a | |||||
| href="/user/{{this.appHelperService.extractId(user.id)}}">{{ user.firstName }} {{ user.lastName }}</a> | |||||
| </span> | |||||
| @@ -0,0 +1,23 @@ | |||||
| import { ComponentFixture, TestBed } from '@angular/core/testing'; | |||||
| import { LinkedLabelComponent } from './linked-label.component'; | |||||
| describe('LinkedLabelComponent', () => { | |||||
| let component: LinkedLabelComponent; | |||||
| let fixture: ComponentFixture<LinkedLabelComponent>; | |||||
| beforeEach(async () => { | |||||
| await TestBed.configureTestingModule({ | |||||
| declarations: [LinkedLabelComponent] | |||||
| }) | |||||
| .compileComponents(); | |||||
| fixture = TestBed.createComponent(LinkedLabelComponent); | |||||
| component = fixture.componentInstance; | |||||
| fixture.detectChanges(); | |||||
| }); | |||||
| it('should create', () => { | |||||
| expect(component).toBeTruthy(); | |||||
| }); | |||||
| }); | |||||
| @@ -0,0 +1,21 @@ | |||||
| import {Component, Input} from '@angular/core'; | |||||
| import {UserJsonld} from "@app/core/api/v1"; | |||||
| import {AppHelperService} from "@app/_helpers/app-helper.service"; | |||||
| @Component({ | |||||
| selector: 'app-linked-label', | |||||
| templateUrl: './linked-label.component.html', | |||||
| styleUrl: './linked-label.component.scss' | |||||
| }) | |||||
| export class LinkedLabelComponent { | |||||
| @Input() public user!: UserJsonld; | |||||
| constructor( | |||||
| protected appHelperService: AppHelperService | |||||
| ) { | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,30 @@ | |||||
| import {ListColTypeAddress} from "@app/_components/list/list-col-type-address"; | |||||
| import {OrderFilter} from "@app/_models/orderFilter"; | |||||
| export interface ListColDefinition { | |||||
| name: string, | |||||
| text: string, | |||||
| type: string, | |||||
| field?: string, | |||||
| multipleFields?: any[], | |||||
| address?: ListColTypeAddress, | |||||
| sortable?: boolean, | |||||
| subResource?: string, | |||||
| sortingFieldName?: string, | |||||
| sortingSubResource?: string, | |||||
| countSortSubresource?: boolean, | |||||
| countSortSubresourceField?: string, | |||||
| countSortSubresourceValue?: any[] | null, | |||||
| countSortFilterSubResource?: string, | |||||
| countSortFilterSubresourceField?: string, | |||||
| countSortFilterSubresourceValue?: string | null, | |||||
| length?: number, | |||||
| displayedLength?: number, | |||||
| groups?: string[], | |||||
| onClickFunction?: Function, | |||||
| updateBooleanOnClick?: boolean, | |||||
| filterType?: string, | |||||
| visible?: boolean, | |||||
| url?: string, | |||||
| filterOptions?: string[], | |||||
| } | |||||
| @@ -0,0 +1,8 @@ | |||||
| export interface ListColTypeAddress { | |||||
| street: string, | |||||
| streetNo: string, | |||||
| zip: string, | |||||
| city: string, | |||||
| country: string, | |||||
| _type: 'address', | |||||
| } | |||||
| @@ -0,0 +1,4 @@ | |||||
| // types.ts | |||||
| import { Observable } from 'rxjs'; | |||||
| export type ListGetDataFunctionType = (index: number, pageSize: number, term?: string) => Observable<any>; | |||||
| @@ -0,0 +1,4 @@ | |||||
| // types.ts | |||||
| import { Observable } from 'rxjs'; | |||||
| export type ListUpdateElementFunctionType = (element: any) => Observable<any>; | |||||
| @@ -0,0 +1,171 @@ | |||||
| <app-paging #pagingComponent | |||||
| [getDataFunction]="getData" | |||||
| [dataSource]="dataSource" | |||||
| [searchable]="searchable" | |||||
| [hidePageSize]="hidePageSize" | |||||
| [displayOptions]="displayOptions" | |||||
| [defaultDisplayOption]="defaultDisplayOption" | |||||
| (displayOptionChange)="onDisplayOptionChange($event)" | |||||
| > | |||||
| <app-toggle *ngIf="showFilterBar && filterExists" | |||||
| [small]="true" | |||||
| [headline]="'basic.filter'" | |||||
| [activeFilterCount]="activeFilterCount" | |||||
| > | |||||
| <app-filter-bar | |||||
| [listColDefinitions]="listColDefinitions" | |||||
| [filterConfig]="filterConfig" | |||||
| (filterInit)="onFilterInit($event)" | |||||
| (filterChanged)="onFilterChanged($event)" | |||||
| (filterSave)="saveFilterConfig()" | |||||
| ></app-filter-bar> | |||||
| </app-toggle> | |||||
| <app-toggle *ngIf="displayedColumns" | |||||
| [small]="true" | |||||
| [headline]="'basic.columns'" | |||||
| > | |||||
| <div class="row align-items-end"> | |||||
| <ng-container *ngFor="let column of listColDefinitions"> | |||||
| <div class="col-4 col-sm-3 col-md-2 col-lg-1 mb-3 switch-widget"> | |||||
| <p class="form-label">{{ column.text | translate }}</p> | |||||
| <label class="switch"> | |||||
| <input type="checkbox" | |||||
| [checked]="column.visible" | |||||
| (change)="onToggleColumnVisibility(column.name)"> | |||||
| <span class="slider round"></span> | |||||
| </label> | |||||
| </div> | |||||
| </ng-container> | |||||
| </div> | |||||
| <div class="row mb-3"> | |||||
| <div class="col-12 d-flex justify-content-end"> | |||||
| <button (click)="saveColumnConfig()" class="btn btn-primary me-3">{{ 'basic.saveSettings' | translate }}</button> | |||||
| <button (click)="showAllColumns()" class="btn btn-primary">{{ 'basic.showAllColumns' | translate }}</button> | |||||
| </div> | |||||
| </div> | |||||
| </app-toggle> | |||||
| <div *ngIf="listColDefinitions" class="table-responsive"> | |||||
| <table mat-table [dataSource]="dataSource" matSort (matSortChange)="onSortChange($event)" class="mat-elevation-z8"> | |||||
| <!-- Iterate over listColDefinitions to define columns --> | |||||
| <ng-container *ngFor="let column of listColDefinitions"> | |||||
| <ng-container [matColumnDef]="column.name"> | |||||
| <!-- Header Cell --> | |||||
| <ng-container *ngIf="column.sortable"> | |||||
| <th mat-header-cell *matHeaderCellDef mat-sort-header> | |||||
| {{ column.text | translate }} | |||||
| </th> | |||||
| </ng-container> | |||||
| <ng-container *ngIf="!column.sortable"> | |||||
| <th mat-header-cell *matHeaderCellDef> | |||||
| {{ column.text | translate }} | |||||
| </th> | |||||
| </ng-container> | |||||
| <!-- Conditional Cells --> | |||||
| <td mat-cell *matCellDef="let element; let rowIndex = index" [ngClass]="getColCssClass(column)"> | |||||
| <ng-container [ngSwitch]="column.type"> | |||||
| <ng-container *ngSwitchCase="COLUMN_TYPE_ADDRESS"> | |||||
| <div [innerHTML]="getElementValue(element, column)"></div> | |||||
| </ng-container> | |||||
| <ng-container *ngSwitchCase="COLUMN_TYPE_BOOLEAN"> | |||||
| <span class="traffic-light" [ngClass]="{ | |||||
| 'green': getElementValue(element, column), | |||||
| 'red': !getElementValue(element, column), | |||||
| 'has-function': !!column.updateBooleanOnClick | |||||
| }" (click)="column.updateBooleanOnClick ? updateBooleanState(element, rowIndex, column) : null"></span> | |||||
| </ng-container> | |||||
| <ng-container *ngSwitchCase="COLUMN_TYPE_CURRENCY"> | |||||
| {{ getElementValue(element, column) | currency: 'EUR' }} | |||||
| </ng-container> | |||||
| <ng-container *ngSwitchCase="COLUMN_TYPE_BTN_DOWNLOAD"> | |||||
| <span class="btn btn-primary bi bi-download p-2-4" | |||||
| data-type="user-tool" data-action="edit" (click)="onDownloadFunction(element)"></span> | |||||
| </ng-container> | |||||
| <ng-container *ngSwitchCase="COLUMN_TYPE_BTN_REMOVE"> | |||||
| <span class="spt-icon-unassign" (click)="onRemoveItemFunction(element, column)"></span> | |||||
| </ng-container> | |||||
| <ng-container *ngSwitchCase="COLUMN_TYPE_BTN_EDIT"> | |||||
| <span class="btn btn-primary bi bi-pencil p-2-4" | |||||
| data-type="user-tool" data-action="edit" (click)="onEditFunction(element)"></span> | |||||
| </ng-container> | |||||
| <ng-container *ngSwitchCase="COLUMN_TYPE_DATE"> | |||||
| {{ getElementValue(element, column) | date:'dd.MM.YYYY - HH:mm':'GMT+0200' }} | |||||
| </ng-container> | |||||
| <ng-container *ngSwitchCase="COLUMN_TYPE_EMAIL"> | |||||
| <span><a href="mailto:{{ getElementValue(element, column) }}">{{ getElementValue(element, column) }}</a></span> | |||||
| </ng-container> | |||||
| <ng-container *ngSwitchCase="COLUMN_TYPE_DETAIL" > | |||||
| <span class="btn btn-primary spt-icon-details" | |||||
| data-type="user-tool" data-action="edit" | |||||
| (click)="onNavigateToDetailsFunction(element, column)"> | |||||
| </span> | |||||
| </ng-container> | |||||
| <ng-container *ngSwitchCase="COLUMN_TYPE_DETAIL_LINK"> | |||||
| <a class="btn btn-primary spt-icon-details" data-type="user-tool" data-a3ction="edit" | |||||
| [routerLink]="[appHelperService.getLink(element, column.url)]"> | |||||
| </a> | |||||
| </ng-container> | |||||
| <ng-container *ngSwitchCase="COLUMN_TYPE_IMAGE"> | |||||
| <img [src]="getElementImage(element, column)" width="40" height="40"/> | |||||
| </ng-container> | |||||
| <ng-container *ngSwitchCase="COLUMN_TYPE_POSITION"> | |||||
| {{ pagingComponent.getPageSize() * (pagingComponent.getPageIndex() - 1) + dataSource.filteredData.indexOf(element) + 1 }} | |||||
| </ng-container> | |||||
| <ng-container *ngSwitchCase="COLUMN_TYPE_TEXT"> | |||||
| {{ getElementValue(element, column) }} | |||||
| </ng-container> | |||||
| <ng-container *ngSwitchCase="COLUMN_TYPE_NUMBER"> | |||||
| {{ getElementValue(element, column) | number:'1.0-0':'de-DE' }} | |||||
| </ng-container> | |||||
| <ng-container *ngSwitchCase="COLUMN_TYPE_NUMBER_UNFORMATTED"> | |||||
| {{ getElementValue(element, column) }} | |||||
| </ng-container> | |||||
| <ng-container *ngSwitchCase="COLUMN_TYPE_NUMBER_BOLD"> | |||||
| <strong>{{ getElementValue(element, column) | number:'1.0-0':'de-DE' }}</strong> | |||||
| </ng-container> | |||||
| <ng-container *ngSwitchCase="COLUMN_TYPE_TEXT_BOLD"> | |||||
| <strong>{{ getElementValue(element, column) }}</strong> | |||||
| </ng-container> | |||||
| <ng-container *ngSwitchCase="COLUMN_TYPE_TEXT_LINKED"> | |||||
| <a [routerLink]="[appHelperService.getResourceLink(element, column.subResource)]"> | |||||
| {{ getElementValue(element, column) }} | |||||
| </a> | |||||
| </ng-container> | |||||
| <ng-container *ngSwitchCase="COLUMN_TYPE_WEBSITE"> | |||||
| <a href="{{ getElementValue(element, column) }}" target="_blank">{{ getElementValue(element, column) }}</a> | |||||
| </ng-container> | |||||
| </ng-container> | |||||
| </td> | |||||
| </ng-container> | |||||
| </ng-container> | |||||
| <!-- Header and Row Definitions --> | |||||
| <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr> | |||||
| <tr mat-row *matRowDef="let row; columns: displayedColumns; index as i;" | |||||
| (click)="onRowSelected(row, i)" | |||||
| [ngClass]="{'highlighted': selectedRowIndex === i}"> | |||||
| </tr> | |||||
| </table> | |||||
| </div> | |||||
| </app-paging> | |||||
| @@ -0,0 +1,23 @@ | |||||
| import { ComponentFixture, TestBed } from '@angular/core/testing'; | |||||
| import { ListComponent } from './list.component'; | |||||
| describe('ListComponent', () => { | |||||
| let component: ListComponent; | |||||
| let fixture: ComponentFixture<ListComponent>; | |||||
| beforeEach(async () => { | |||||
| await TestBed.configureTestingModule({ | |||||
| declarations: [ListComponent] | |||||
| }) | |||||
| .compileComponents(); | |||||
| fixture = TestBed.createComponent(ListComponent); | |||||
| component = fixture.componentInstance; | |||||
| fixture.detectChanges(); | |||||
| }); | |||||
| it('should create', () => { | |||||
| expect(component).toBeTruthy(); | |||||
| }); | |||||
| }); | |||||
| @@ -0,0 +1,474 @@ | |||||
| import {AfterViewInit, Component, Input, OnDestroy, OnInit, ViewChild} from '@angular/core'; | |||||
| import {MatSort, Sort} from "@angular/material/sort"; | |||||
| import {PagingComponent} from "@app/_components/paging/paging.component"; | |||||
| import {MatTableDataSource} from "@angular/material/table"; | |||||
| import {ListColDefinition} from "@app/_components/list/list-col-definition"; | |||||
| import {AppHelperService} from "@app/_helpers/app-helper.service"; | |||||
| import {ListGetDataFunctionType} from "@app/_components/list/list-get-data-function-type"; | |||||
| import {ListUpdateElementFunctionType} from "@app/_components/list/list-update-element-function-type"; | |||||
| import {FilterBarComponent} from "@app/_components/filter-bar/filter-bar.component"; | |||||
| import { Router } from '@angular/router'; | |||||
| import {interval, Subscription} from "rxjs"; | |||||
| @Component({ | |||||
| selector: 'app-list', | |||||
| templateUrl: './list.component.html', | |||||
| styleUrl: './list.component.scss' | |||||
| }) | |||||
| export class ListComponent implements OnInit, AfterViewInit, OnDestroy { | |||||
| @Input() public listId!: string; | |||||
| @Input() public getDataFunction!: ListGetDataFunctionType; | |||||
| @Input() public onSortFunction!: Function; | |||||
| @Input() public onNavigateToDetailsFunction!: Function; | |||||
| @Input() public onRemoveItemFunction!: Function; | |||||
| @Input() public onEditFunction!: Function; | |||||
| @Input() public onDownloadFunction!: Function; | |||||
| @Input() public onRowSelectedFunction!: Function; | |||||
| @Input() public onUpdateBooleanStateFunction!: ListUpdateElementFunctionType; | |||||
| @Input() public searchable: boolean; | |||||
| @Input() public showDetailButton: boolean; | |||||
| @Input() public showPosition: boolean; | |||||
| @Input() public showFilterBar: boolean; | |||||
| @Input() public listColDefinitions!: ListColDefinition[]; | |||||
| @Input() public hidePageSize: boolean; | |||||
| @Input() public displayOptions!: { [key: string]: string }; | |||||
| @Input() public defaultDisplayOption!: string; | |||||
| @Input() public refreshIntervalSeconds?: number; | |||||
| @ViewChild(MatSort) sort; | |||||
| @ViewChild("pagingComponent", {static: false}) protected pagingComponent!: PagingComponent; | |||||
| @ViewChild("filterBarComponent", {static: false}) protected filterBarComponent!: FilterBarComponent; | |||||
| public static COLUMN_TYPE_ADDRESS: string = 'address'; | |||||
| public static COLUMN_TYPE_BOOLEAN: string = 'boolean'; | |||||
| public static COLUMN_TYPE_BTN_DOWNLOAD: string = 'btn_download'; | |||||
| public static COLUMN_TYPE_BTN_EDIT: string = 'btn_edit'; | |||||
| public static COLUMN_TYPE_BTN_REMOVE: string = 'btn_remove'; | |||||
| public static COLUMN_TYPE_CURRENCY: string = 'euro'; | |||||
| public static COLUMN_TYPE_DATE: string = 'date'; | |||||
| public static COLUMN_TYPE_DETAIL: string = 'detail'; | |||||
| public static COLUMN_TYPE_DETAIL_LINK: string = 'detail_link'; | |||||
| public static COLUMN_TYPE_EMAIL: string = 'email'; | |||||
| public static COLUMN_TYPE_IMAGE: string = 'image'; | |||||
| public static COLUMN_TYPE_COMBINED_IMAGES: string = 'combined_images'; | |||||
| public static COLUMN_TYPE_NUMBER: string = 'number'; | |||||
| public static COLUMN_TYPE_NUMBER_UNFORMATTED: string = 'number_unformatted'; | |||||
| public static COLUMN_TYPE_NUMBER_BOLD: string = 'number_bold'; | |||||
| public static COLUMN_TYPE_POSITION: string = 'position'; | |||||
| public static COLUMN_TYPE_TEXT: string = 'text'; | |||||
| public static COLUMN_TYPE_TEXT_BOLD: string = 'text_bold'; | |||||
| public static COLUMN_TYPE_TEXT_LINKED: string = 'text_linked'; | |||||
| public static COLUMN_TYPE_WEBSITE: string = 'website'; | |||||
| public activeFilterCount: number = 0; | |||||
| protected displayedColumns!: string[]; | |||||
| protected selectedRowIndex: number | null = null; | |||||
| protected dataSource; | |||||
| protected currentGroup!: string; | |||||
| protected filterExists!: boolean; | |||||
| protected sortObj!: any; | |||||
| protected filterObj!: any; | |||||
| protected listColDefinitionsByField!: any; | |||||
| protected filterConfig: string | null; | |||||
| private refreshSubscription?: Subscription; | |||||
| constructor( | |||||
| protected appHelperService: AppHelperService, | |||||
| private router: Router, | |||||
| ) { | |||||
| this.searchable = true; | |||||
| this.showDetailButton = true; | |||||
| this.showPosition = true; | |||||
| this.showFilterBar = true; | |||||
| this.filterExists = false; | |||||
| this.filterObj = {}; | |||||
| this.sort = new MatSort(); | |||||
| this.hidePageSize = false; | |||||
| this.dataSource = new MatTableDataSource<any>(); | |||||
| this.filterConfig = null; | |||||
| } | |||||
| ngOnInit(): void { | |||||
| this.loadColumnConfig(); | |||||
| if (this.showPosition) { | |||||
| this.listColDefinitions.unshift(ListComponent.getDefaultColPosition()); | |||||
| } | |||||
| if (this.showDetailButton) { | |||||
| // this.listColDefinitions.unshift(ListComponent.getDefaultColDetailBtn()); | |||||
| this.listColDefinitions.unshift(ListComponent.getDefaultColDetailBtnLink(this.router.routerState.snapshot.url)); | |||||
| } | |||||
| if (this.displayOptions !== undefined) { | |||||
| this.currentGroup = this.defaultDisplayOption || Object.keys(this.displayOptions)[0] || ''; | |||||
| } | |||||
| this.listColDefinitionsByField = {}; | |||||
| this.listColDefinitions.forEach((value, index) => { | |||||
| if (value.visible === undefined) { | |||||
| value.visible = true; | |||||
| } | |||||
| this.listColDefinitionsByField[value['name']] = value; | |||||
| if (value.filterType !== undefined) { | |||||
| this.filterExists = true; | |||||
| } | |||||
| }) | |||||
| this.updateDisplayedColumns(); | |||||
| this.filterConfig = this.loadFilterConfig(); | |||||
| this.setupAutoRefresh(); | |||||
| } | |||||
| private setupAutoRefresh(): void { | |||||
| this.clearAutoRefresh(); | |||||
| if (this.refreshIntervalSeconds && this.refreshIntervalSeconds > 0) { | |||||
| this.refreshSubscription = interval(this.refreshIntervalSeconds * 1000).subscribe(() => { | |||||
| this.getData(); | |||||
| }); | |||||
| } | |||||
| } | |||||
| private clearAutoRefresh(): void { | |||||
| if (this.refreshSubscription) { | |||||
| this.refreshSubscription.unsubscribe(); | |||||
| } | |||||
| } | |||||
| saveFilterConfig(): void { | |||||
| localStorage.setItem(`filterConfig_${this.listId}`, this.getFilterJsonString()); | |||||
| } | |||||
| loadFilterConfig(): string | null { | |||||
| return localStorage.getItem(`filterConfig_${this.listId}`); | |||||
| } | |||||
| saveColumnConfig(): void { | |||||
| const config = this.listColDefinitions.map(col => ({ | |||||
| name: col.name, | |||||
| visible: col.visible | |||||
| })); | |||||
| localStorage.setItem(`listConfig_${this.listId}`, JSON.stringify(config)); | |||||
| } | |||||
| loadColumnConfig(): void { | |||||
| const savedConfig = localStorage.getItem(`listConfig_${this.listId}`); | |||||
| if (savedConfig) { | |||||
| const config = JSON.parse(savedConfig); | |||||
| this.listColDefinitions.forEach(col => { | |||||
| const savedCol = config.find((c: any) => c.name === col.name); | |||||
| if (savedCol) { | |||||
| col.visible = savedCol.visible; | |||||
| } | |||||
| }); | |||||
| this.updateDisplayedColumns(); | |||||
| } | |||||
| } | |||||
| updateDisplayedColumns(): void { | |||||
| this.displayedColumns = this.listColDefinitions | |||||
| .filter(col => col.visible !== false && | |||||
| (this.displayOptions === undefined || | |||||
| col.groups?.includes(this.currentGroup) || | |||||
| col.type === ListComponent.COLUMN_TYPE_DETAIL || | |||||
| col.type === ListComponent.COLUMN_TYPE_POSITION)) | |||||
| .map(col => col.name); | |||||
| } | |||||
| onDisplayOptionChange(option: string): void { | |||||
| this.currentGroup = option; | |||||
| this.updateDisplayedColumns(); | |||||
| } | |||||
| onToggleColumnVisibility(columnName: string): void { | |||||
| const column = this.listColDefinitions.find(col => col.name === columnName); | |||||
| if (column) { | |||||
| column.visible = !column.visible; | |||||
| this.updateDisplayedColumns(); | |||||
| } | |||||
| } | |||||
| showAllColumns() { | |||||
| this.listColDefinitions.forEach((value, index) => { | |||||
| value.visible = true; | |||||
| }); | |||||
| this.updateDisplayedColumns(); | |||||
| } | |||||
| getColumnVisibility(): { [key: string]: boolean } { | |||||
| const visibility: { [key: string]: boolean } = {}; | |||||
| this.listColDefinitions.forEach(col => { | |||||
| visibility[col.name] = col.visible !== false; | |||||
| }); | |||||
| return visibility; | |||||
| } | |||||
| ngAfterViewInit(): void { | |||||
| } | |||||
| getData = (): void => { | |||||
| this.getDataFunction( | |||||
| this.pagingComponent.getPageIndex(), | |||||
| this.pagingComponent.getPageSize(), | |||||
| this.pagingComponent.getSearchValue() | |||||
| ).subscribe( | |||||
| data => { | |||||
| this.dataSource = new MatTableDataSource<any>(data['hydra:member']); | |||||
| this.pagingComponent.setDataLength(data["hydra:totalItems"]); | |||||
| } | |||||
| ) | |||||
| } | |||||
| onSortChange = (sortState: Sort) => { | |||||
| let listColDefinition: any = this.listColDefinitionsByField[sortState.active]; | |||||
| this.sortObj = sortState; | |||||
| this.sortObj['listColDefinition'] = listColDefinition; | |||||
| this.pagingComponent.resetPageIndex(); | |||||
| this.onSortFunction(sortState); | |||||
| this.getData(); | |||||
| } | |||||
| onRowSelected(row: any, index: number) { | |||||
| this.selectedRowIndex = index; | |||||
| if (this.onRowSelectedFunction !== undefined) { | |||||
| this.onRowSelectedFunction(row, index); | |||||
| } | |||||
| } | |||||
| getElementValue(element: any, column: ListColDefinition, multipleFieldIndex?: number): any | null { | |||||
| element = column.subResource !== undefined ? element[column.subResource] : element; | |||||
| if (element === undefined) { | |||||
| return null; | |||||
| } | |||||
| if (column.field !== undefined) { | |||||
| if ( | |||||
| column.displayedLength !== undefined && | |||||
| element[column.field] !== undefined && | |||||
| element[column.field].length > column.displayedLength | |||||
| ) { | |||||
| return element[column.field]?.slice(0, column.displayedLength) + '...'; | |||||
| } | |||||
| return element[column.field]; | |||||
| } | |||||
| if (column.multipleFields !== undefined) { | |||||
| if (multipleFieldIndex !== undefined) { | |||||
| return element[column.multipleFields[multipleFieldIndex]]; | |||||
| } | |||||
| let res: any[] = []; | |||||
| column.multipleFields.forEach((field, index) => { | |||||
| res.push(element[field]); | |||||
| }) | |||||
| return res; | |||||
| } | |||||
| if (column.address !== undefined) { | |||||
| const field = column.address; | |||||
| let addressString = ''; | |||||
| if (element[field.street] !== undefined && element[field.street] !== null) { | |||||
| addressString += `${element[field.street].trim()} `; | |||||
| } | |||||
| if (element[field.streetNo] !== undefined && element[field.streetNo] !== null) { | |||||
| addressString += `${element[field.streetNo].trim()} `; | |||||
| } | |||||
| addressString += ' <br/> '; | |||||
| if (element[field.zip] !== undefined && element[field.zip] !== null) { | |||||
| addressString += `${element[field.zip].trim()} `; | |||||
| } | |||||
| if (element[field.city] !== undefined && element[field.city] !== null) { | |||||
| addressString += `${element[field.city].trim()}`; | |||||
| } | |||||
| addressString += ' <br/> '; | |||||
| if (element[field.country] !== undefined && element[field.country] !== null) { | |||||
| addressString += `${element[field.country].trim()}`; | |||||
| } | |||||
| return addressString; | |||||
| } | |||||
| return element; | |||||
| } | |||||
| getElementImage(element: any, column: ListColDefinition): any { | |||||
| let elementValue = this.getElementValue(element, column); | |||||
| if (elementValue !== undefined && elementValue !== null) { | |||||
| return elementValue; | |||||
| } | |||||
| return "/assets/images/icons/dummy-product.png" | |||||
| } | |||||
| getColCssClass(column: ListColDefinition): string { | |||||
| switch (column.type) { | |||||
| case ListComponent.COLUMN_TYPE_DETAIL: | |||||
| return "spt-button-td"; | |||||
| case ListComponent.COLUMN_TYPE_BTN_REMOVE: | |||||
| return "spt-button-td text-end"; | |||||
| case ListComponent.COLUMN_TYPE_TEXT: | |||||
| return "spt-version-td"; | |||||
| default: | |||||
| return ""; | |||||
| } | |||||
| } | |||||
| public getPageIndex() { | |||||
| return this.pagingComponent.getPageIndex(); | |||||
| } | |||||
| public getPageSize() { | |||||
| return this.pagingComponent.getPageSize(); | |||||
| } | |||||
| public static getDefaultColDetailBtn(): ListColDefinition { | |||||
| return { | |||||
| name: 'detail', | |||||
| text: '', | |||||
| type: ListComponent.COLUMN_TYPE_DETAIL | |||||
| } as ListColDefinition; | |||||
| } | |||||
| public static getDefaultColDetailBtnLink(currentUrl: string): ListColDefinition { | |||||
| return { | |||||
| name: 'detaillink', | |||||
| text: '', | |||||
| url: currentUrl, | |||||
| type: ListComponent.COLUMN_TYPE_DETAIL_LINK | |||||
| } as ListColDefinition; | |||||
| } | |||||
| public static getDefaultColPosition(): ListColDefinition { | |||||
| return { | |||||
| name: 'pos', | |||||
| text: 'overview.number', | |||||
| type: ListComponent.COLUMN_TYPE_POSITION | |||||
| } as ListColDefinition; | |||||
| } | |||||
| public getSortingJsonString(): any | |||||
| { | |||||
| return JSON.stringify(this.sortObj); | |||||
| } | |||||
| public updateBooleanState = (element: any, index: number, column: ListColDefinition) => { | |||||
| if (this.onUpdateBooleanStateFunction === undefined) { | |||||
| throw new Error('no onUpdateBooleanStateFunction given'); | |||||
| } | |||||
| if (column.field !== undefined) { | |||||
| element[column.field] = !element[column.field]; | |||||
| } else { | |||||
| throw new Error('column.field is undefined'); | |||||
| } | |||||
| this.onUpdateBooleanStateFunction(element).subscribe( | |||||
| data => { | |||||
| this.updateRow(data, index); | |||||
| } | |||||
| ) | |||||
| } | |||||
| public updateRow(element: any, index: number) { | |||||
| const data = this.dataSource.data as any; | |||||
| data[index] = element; | |||||
| this.dataSource.data = data; | |||||
| } | |||||
| public onFilterInit(filterData: {filters: any, activeCount: number}) { | |||||
| this.filterObj = filterData.filters; | |||||
| this.activeFilterCount = filterData.activeCount; | |||||
| } | |||||
| public onFilterChanged(filterData: {filters: any, activeCount: number}) { | |||||
| console.log(filterData); | |||||
| const filterJson = JSON.stringify(filterData.filters); | |||||
| const currentFilterJson = JSON.stringify(this.filterObj); | |||||
| if (filterJson !== currentFilterJson) { | |||||
| this.filterObj = filterData.filters; | |||||
| this.activeFilterCount = filterData.activeCount; | |||||
| this.getData(); | |||||
| } | |||||
| } | |||||
| public getFilterJsonString(): any | |||||
| { | |||||
| return JSON.stringify(this.filterObj); | |||||
| } | |||||
| get COLUMN_TYPE_ADDRESS(): string { | |||||
| return ListComponent.COLUMN_TYPE_ADDRESS; | |||||
| } | |||||
| get COLUMN_TYPE_BOOLEAN(): string { | |||||
| return ListComponent.COLUMN_TYPE_BOOLEAN; | |||||
| } | |||||
| get COLUMN_TYPE_BTN_DOWNLOAD(): string { | |||||
| return ListComponent.COLUMN_TYPE_BTN_DOWNLOAD; | |||||
| } | |||||
| get COLUMN_TYPE_BTN_EDIT(): string { | |||||
| return ListComponent.COLUMN_TYPE_BTN_EDIT; | |||||
| } | |||||
| get COLUMN_TYPE_BTN_REMOVE(): string { | |||||
| return ListComponent.COLUMN_TYPE_BTN_REMOVE; | |||||
| } | |||||
| get COLUMN_TYPE_CURRENCY(): string { | |||||
| return ListComponent.COLUMN_TYPE_CURRENCY; | |||||
| } | |||||
| get COLUMN_TYPE_DATE(): string { | |||||
| return ListComponent.COLUMN_TYPE_DATE; | |||||
| } | |||||
| get COLUMN_TYPE_DETAIL(): string { | |||||
| return ListComponent.COLUMN_TYPE_DETAIL; | |||||
| } | |||||
| get COLUMN_TYPE_DETAIL_LINK(): string { | |||||
| return ListComponent.COLUMN_TYPE_DETAIL_LINK; | |||||
| } | |||||
| get COLUMN_TYPE_EMAIL(): string { | |||||
| return ListComponent.COLUMN_TYPE_EMAIL; | |||||
| } | |||||
| get COLUMN_TYPE_POSITION(): string { | |||||
| return ListComponent.COLUMN_TYPE_POSITION; | |||||
| } | |||||
| get COLUMN_TYPE_IMAGE(): string { | |||||
| return ListComponent.COLUMN_TYPE_IMAGE; | |||||
| } | |||||
| get COLUMN_TYPE_COMBINED_IMAGES(): string { | |||||
| return ListComponent.COLUMN_TYPE_COMBINED_IMAGES; | |||||
| } | |||||
| get COLUMN_TYPE_TEXT(): string { | |||||
| return ListComponent.COLUMN_TYPE_TEXT; | |||||
| } | |||||
| get COLUMN_TYPE_NUMBER(): string { | |||||
| return ListComponent.COLUMN_TYPE_NUMBER; | |||||
| } | |||||
| get COLUMN_TYPE_NUMBER_UNFORMATTED(): string { | |||||
| return ListComponent.COLUMN_TYPE_NUMBER_UNFORMATTED; | |||||
| } | |||||
| get COLUMN_TYPE_NUMBER_BOLD(): string { | |||||
| return ListComponent.COLUMN_TYPE_NUMBER_BOLD; | |||||
| } | |||||
| get COLUMN_TYPE_TEXT_BOLD(): string { | |||||
| return ListComponent.COLUMN_TYPE_TEXT_BOLD; | |||||
| } | |||||
| get COLUMN_TYPE_TEXT_LINKED(): string { | |||||
| return ListComponent.COLUMN_TYPE_TEXT_LINKED; | |||||
| } | |||||
| get COLUMN_TYPE_WEBSITE(): string { | |||||
| return ListComponent.COLUMN_TYPE_WEBSITE; | |||||
| } | |||||
| ngOnDestroy(): void { | |||||
| this.clearAutoRefresh(); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,52 @@ | |||||
| <div class="spt-tools"> | |||||
| <ng-container *ngIf="searchable"> | |||||
| <div class="spt-form"> | |||||
| <form [formGroup]="searchForm" class="position-relative"> | |||||
| <div class="row"> | |||||
| <div class="col-12"> | |||||
| <input type="text" class="form-control" formControlName="inputText" | |||||
| placeholder="{{'form.search_placeholder' | translate}}"> | |||||
| <span class="spt-clear" *ngIf="searchForm.get('inputText')?.value" (click)="clearForm()"></span> | |||||
| </div> | |||||
| </div> | |||||
| </form> | |||||
| </div> | |||||
| </ng-container> | |||||
| <div *ngIf="displayOptionKeys.length > 1"> | |||||
| <select [value]="selectedDisplayOption" (change)="onDisplayOptionChange($any($event.target).value)"> | |||||
| <option *ngFor="let option of displayOptionKeys" [value]="option"> | |||||
| {{displayOptions[option]}} | |||||
| </option> | |||||
| </select> | |||||
| </div> | |||||
| <div class="d-flex align-items-center flex-wrap"> | |||||
| <mat-paginator *ngIf="dataLength > 0" class="" | |||||
| [pageSizeOptions]="pageSizeOptions" | |||||
| [length]="dataLength" | |||||
| (page)="handlePageEvent($event)" | |||||
| [pageSize]="pageSize" | |||||
| [pageIndex]="pageIndex" | |||||
| [hidePageSize]="hidePageSize" | |||||
| showFirstLastButtons | |||||
| > | |||||
| </mat-paginator> | |||||
| <button type="button" class="btn btn-primary ms-3" | |||||
| (click)="getData()"><span class="bi bi-arrow-clockwise"> {{ 'basic.refresh' | translate }}</span> | |||||
| </button> | |||||
| </div> | |||||
| </div> | |||||
| <ng-content></ng-content> | |||||
| <div *ngIf="dataInitialized && dataLength <= 0" class="spt-no-entries">{{'form.no_data' | translate}}</div> | |||||
| <div class="spt-tools single"> | |||||
| <mat-paginator *ngIf="dataLength > 0" class="" | |||||
| [pageSizeOptions]="pageSizeOptions" | |||||
| [length]="dataLength" | |||||
| (page)="handlePageEvent($event)" | |||||
| [pageSize]="pageSize" | |||||
| [pageIndex]="pageIndex" | |||||
| [hidePageSize]="hidePageSize" | |||||
| showFirstLastButtons> | |||||
| </mat-paginator> | |||||
| </div> | |||||
| @@ -0,0 +1,23 @@ | |||||
| import { ComponentFixture, TestBed } from '@angular/core/testing'; | |||||
| import { PagingComponent } from './paging.component'; | |||||
| describe('ListComponent', () => { | |||||
| let component: PagingComponent; | |||||
| let fixture: ComponentFixture<PagingComponent>; | |||||
| beforeEach(async () => { | |||||
| await TestBed.configureTestingModule({ | |||||
| declarations: [PagingComponent] | |||||
| }) | |||||
| .compileComponents(); | |||||
| fixture = TestBed.createComponent(PagingComponent); | |||||
| component = fixture.componentInstance; | |||||
| fixture.detectChanges(); | |||||
| }); | |||||
| it('should create', () => { | |||||
| expect(component).toBeTruthy(); | |||||
| }); | |||||
| }); | |||||
| @@ -0,0 +1,132 @@ | |||||
| import { | |||||
| AfterViewInit, | |||||
| ChangeDetectorRef, | |||||
| Component, | |||||
| EventEmitter, | |||||
| Input, | |||||
| OnInit, | |||||
| Output, | |||||
| ViewChild | |||||
| } from '@angular/core'; | |||||
| import {MatPaginator, MatPaginatorIntl, PageEvent} from "@angular/material/paginator"; | |||||
| import {FormBuilder, FormGroup} from "@angular/forms"; | |||||
| import {debounceTime, distinctUntilChanged} from "rxjs"; | |||||
| @Component({ | |||||
| selector: 'app-paging', | |||||
| templateUrl: './paging.component.html', | |||||
| styleUrl: './paging.component.scss' | |||||
| }) | |||||
| export class PagingComponent implements OnInit, AfterViewInit { | |||||
| @Input() public dataSource!: any; | |||||
| @Input() public getDataFunction!: Function; | |||||
| @Input() public pageSize!: number; | |||||
| @Input() public pageSizeOptions!: number[]; | |||||
| @Input() public searchable: boolean; | |||||
| @Input() public hidePageSize: boolean; | |||||
| @Input() public displayOptions!: { [key: string]: any }; | |||||
| @Input() public defaultDisplayOption!: string; | |||||
| @Output() public displayOptionChange = new EventEmitter<string>(); | |||||
| @ViewChild(MatPaginator) public paginator!: MatPaginator; | |||||
| protected defaultPageSize: number = 50; | |||||
| protected defaultPageSizeOptions: number[] = [20, 50, 100]; | |||||
| protected dataLength: number; | |||||
| protected pageEvent: PageEvent; | |||||
| protected pageIndex: number; | |||||
| protected searchForm!: FormGroup; | |||||
| protected dataInitialized: boolean = false; | |||||
| protected selectedDisplayOption: string = ''; | |||||
| protected displayOptionKeys: string[] = []; | |||||
| constructor( | |||||
| private fb: FormBuilder | |||||
| ) { | |||||
| this.dataLength = 0; | |||||
| this.pageEvent = new PageEvent(); | |||||
| this.pageIndex = 0; | |||||
| this.searchable = false; | |||||
| this.hidePageSize = false; | |||||
| } | |||||
| ngOnInit() { | |||||
| this.pageSize = this.pageSize !== undefined ? this.pageSize : this.defaultPageSize; | |||||
| this.pageSizeOptions = this.pageSizeOptions !== undefined ? this.pageSizeOptions : this.defaultPageSizeOptions; | |||||
| this.paginator = new MatPaginator(new MatPaginatorIntl(), ChangeDetectorRef.prototype); | |||||
| if (this.searchable) { | |||||
| this.searchForm = this.fb.group({ | |||||
| inputText: [''] | |||||
| }); | |||||
| this.searchForm.get('inputText')?.valueChanges.pipe( | |||||
| debounceTime(1000), | |||||
| distinctUntilChanged() | |||||
| ).subscribe(value => { | |||||
| this.resetPageIndex(); | |||||
| this.getData(); | |||||
| }); | |||||
| } | |||||
| if (this.displayOptions !== undefined) { | |||||
| this.displayOptionKeys = Object.keys(this.displayOptions); | |||||
| this.selectedDisplayOption = this.defaultDisplayOption || Object.keys(this.displayOptions)[0] || ''; | |||||
| } | |||||
| } | |||||
| ngAfterViewInit() { | |||||
| } | |||||
| getData() { | |||||
| this.getDataFunction( | |||||
| this.getPageIndex(), | |||||
| this.getPageSize(), | |||||
| this.searchForm ? this.searchForm.get('inputText')?.value : undefined | |||||
| ); | |||||
| } | |||||
| handlePageEvent(e: PageEvent) { | |||||
| this.pageEvent = e; | |||||
| this.dataLength = e.length; | |||||
| this.pageIndex = e.pageIndex.valueOf(); | |||||
| this.pageSize = e.pageSize.valueOf(); | |||||
| this.getData(); | |||||
| } | |||||
| resetPageIndex(): void { | |||||
| this.pageIndex = 0; | |||||
| } | |||||
| getPageIndex(): number { | |||||
| return this.pageIndex + 1; | |||||
| } | |||||
| getPageSize(): number { | |||||
| return this.pageSize; | |||||
| } | |||||
| setDataLength(dataLength: number): void { | |||||
| this.dataInitialized = true; | |||||
| this.dataLength = dataLength; | |||||
| } | |||||
| clearForm() { | |||||
| this.searchForm.get('inputText')!.setValue(''); | |||||
| } | |||||
| getSearchValue() { | |||||
| if (this.searchable) { | |||||
| return this.searchForm.get('inputText')!.value; | |||||
| } | |||||
| return undefined; | |||||
| } | |||||
| onDisplayOptionChange(event: Event): void { | |||||
| this.displayOptionChange.emit(String(event)); | |||||
| } | |||||
| protected readonly Object = Object; | |||||
| } | |||||
| @@ -0,0 +1,7 @@ | |||||
| export interface SearchInputColDef { | |||||
| column: string, | |||||
| columnHeader: string, | |||||
| columnType: string, | |||||
| field?: string | |||||
| subResource?: string | |||||
| } | |||||
| @@ -0,0 +1,11 @@ | |||||
| <label for="{{ dataField }}" class="form-label"> | |||||
| {{ formLabelLangKey | translate }}: | |||||
| </label> | |||||
| <input type="text" class="form-control" id="{{dataField}}" | |||||
| [ngbTypeahead]="searchItem" | |||||
| [inputFormatter]="formatter" | |||||
| [value]="documentForm.get(documentFormField)?.value" | |||||
| [resultFormatter]="formatter" | |||||
| [editable]="false" | |||||
| (selectItem)="onItemSelect($event)" | |||||
| /> | |||||
| @@ -0,0 +1,23 @@ | |||||
| import { ComponentFixture, TestBed } from '@angular/core/testing'; | |||||
| import { SearchInputComponent } from './search-input.component'; | |||||
| describe('SearchInputComponent', () => { | |||||
| let component: SearchInputComponent; | |||||
| let fixture: ComponentFixture<SearchInputComponent>; | |||||
| beforeEach(async () => { | |||||
| await TestBed.configureTestingModule({ | |||||
| declarations: [SearchInputComponent] | |||||
| }) | |||||
| .compileComponents(); | |||||
| fixture = TestBed.createComponent(SearchInputComponent); | |||||
| component = fixture.componentInstance; | |||||
| fixture.detectChanges(); | |||||
| }); | |||||
| it('should create', () => { | |||||
| expect(component).toBeTruthy(); | |||||
| }); | |||||
| }); | |||||
| @@ -0,0 +1,38 @@ | |||||
| import {Component, Input} from '@angular/core'; | |||||
| import {FormGroup} from "@angular/forms"; | |||||
| import {debounceTime, distinctUntilChanged, Observable, OperatorFunction, switchMap} from "rxjs"; | |||||
| import {filter, map} from "rxjs/operators"; | |||||
| @Component({ | |||||
| selector: 'app-search-input', | |||||
| templateUrl: './search-input.component.html', | |||||
| styleUrl: './search-input.component.scss' | |||||
| }) | |||||
| export class SearchInputComponent { | |||||
| @Input() public formId!: string; | |||||
| @Input() public formLabelLangKey!: string; | |||||
| @Input() public dataField!: string; | |||||
| @Input() public documentForm!: FormGroup; | |||||
| @Input() public documentFormField!: string; | |||||
| @Input() public fetchFunction!: (term: string) => Observable<{ id: any; name: any }[]>; | |||||
| protected formatter = (apiData: any) => apiData.name; | |||||
| protected searchItem: OperatorFunction<string, readonly { | |||||
| id: any; | |||||
| name: any | |||||
| }[]> = (text$: Observable<string>) => | |||||
| text$.pipe( | |||||
| debounceTime(200), | |||||
| distinctUntilChanged(), | |||||
| filter((term) => term.length >= 2), | |||||
| switchMap((term) => this.fetchFunction(term)), | |||||
| map((items: {id: any, name: any}[]) => items.slice(0, 10)), | |||||
| ); | |||||
| protected onItemSelect(selectedItem: any): void { | |||||
| this.documentForm.get(this.formId)?.setValue(selectedItem.item.id); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,19 @@ | |||||
| <div class="search-select" [class.hide-header]="!showHeader"> | |||||
| <div class="show-name" *ngIf="!displayAsButton"> | |||||
| <p #paragraphRef (click)="openSearchBox()" [class.search-empty]="!searchBoxFilled"></p> | |||||
| <span class="spt-clear" *ngIf="searchBoxFilled" (click)="clearSearch()"></span> | |||||
| </div> | |||||
| <div *ngIf="displayAsButton" (click)="openSearchBox()" class="btn btn-primary mb-2">{{ 'basic.pleaseChoose' | translate }}</div> | |||||
| <div class="search-toggle" [class.search-box-open]="searchBoxOpen"> | |||||
| <app-list #listComponent | |||||
| [listId]="'searchSelect'" | |||||
| [getDataFunction]="getDataFunction" | |||||
| [onRowSelectedFunction]="onRowSelected" | |||||
| [listColDefinitions]="listColDefinitions" | |||||
| [showDetailButton]="false" | |||||
| [showPosition]="false" | |||||
| [onSortFunction]="onSortChange" | |||||
| > | |||||
| </app-list> | |||||
| </div> | |||||
| </div> | |||||
| @@ -0,0 +1,23 @@ | |||||
| import { ComponentFixture, TestBed } from '@angular/core/testing'; | |||||
| import { SearchSelectComponent } from './search-select.component'; | |||||
| describe('SearchSelectComponent', () => { | |||||
| let component: SearchSelectComponent; | |||||
| let fixture: ComponentFixture<SearchSelectComponent>; | |||||
| beforeEach(async () => { | |||||
| await TestBed.configureTestingModule({ | |||||
| declarations: [SearchSelectComponent] | |||||
| }) | |||||
| .compileComponents(); | |||||
| fixture = TestBed.createComponent(SearchSelectComponent); | |||||
| component = fixture.componentInstance; | |||||
| fixture.detectChanges(); | |||||
| }); | |||||
| it('should create', () => { | |||||
| expect(component).toBeTruthy(); | |||||
| }); | |||||
| }); | |||||
| @@ -0,0 +1,253 @@ | |||||
| import {AfterViewInit, Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild} from '@angular/core'; | |||||
| import {FormGroup} from "@angular/forms"; | |||||
| import {ListComponent} from "@app/_components/list/list.component"; | |||||
| import {ListColDefinition} from "@app/_components/list/list-col-definition"; | |||||
| import {Observable} from "rxjs"; | |||||
| import {Sort} from "@angular/material/sort"; | |||||
| import {FilterBarComponent} from "@app/_components/filter-bar/filter-bar.component"; | |||||
| @Component({ | |||||
| selector: 'app-search-select', | |||||
| templateUrl: './search-select.component.html', | |||||
| styleUrl: './search-select.component.scss' | |||||
| }) | |||||
| export class SearchSelectComponent implements OnInit, AfterViewInit { | |||||
| @Input() public formId!: string; | |||||
| @Input() public resultField!: string; | |||||
| @Input() public formLabelLangKey!: string; | |||||
| @Input() public documentForm!: FormGroup; | |||||
| @Input() public documentFormField!: string; | |||||
| @Input() public getDataFunction!: (index: number, pageSize: number, term?: string) => Observable<any>; | |||||
| @Input() public onRowSelectedFunction!: Function; | |||||
| @Input() public dataSet!: any; | |||||
| @Input() public displayedDataField!: string; | |||||
| @Input() public displayedDataSubResource!: string; | |||||
| @Input() public listColDefinitions!: ListColDefinition[]; | |||||
| @Input() public displayAsButton: boolean; | |||||
| @Input() public showHeader: boolean; | |||||
| @Output() rowSelected = new EventEmitter<any>(); | |||||
| @ViewChild('paragraphRef', {static: false}) paragraphRef!: ElementRef; | |||||
| @ViewChild("listComponent", {static: false}) listComponent!: ListComponent; | |||||
| protected readonly SearchSelectComponent = SearchSelectComponent; | |||||
| protected selectedRowIndex: number | null = null; | |||||
| protected searchBoxOpen: boolean; | |||||
| protected searchBoxInitialized: boolean; | |||||
| protected searchBoxFilled: boolean; | |||||
| constructor() { | |||||
| this.searchBoxOpen = false; | |||||
| this.searchBoxInitialized = false; | |||||
| this.searchBoxFilled = false; | |||||
| this.displayAsButton = false; | |||||
| this.showHeader = false; | |||||
| } | |||||
| ngOnInit(): void { | |||||
| if (this.dataSet !== undefined) { | |||||
| this.searchBoxFilled = true; | |||||
| } | |||||
| } | |||||
| ngAfterViewInit(): void { | |||||
| if (this.dataSet !== undefined) { | |||||
| this.paragraphRef.nativeElement.textContent = this.dataSet[this.displayedDataField]; | |||||
| } | |||||
| } | |||||
| onRowSelected = (row: any, index: number) => { | |||||
| if (this.onRowSelectedFunction !== undefined) { | |||||
| this.onRowSelectedFunction(row, index); | |||||
| } else { | |||||
| this.selectedRowIndex = index; | |||||
| const value = this.resultField !== undefined ? row[this.resultField] : row.id; | |||||
| this.documentForm.get(this.formId)?.setValue(value); | |||||
| if (this.displayedDataSubResource !== undefined) { | |||||
| this.paragraphRef.nativeElement.textContent = row[this.displayedDataSubResource][this.displayedDataField]; | |||||
| } else { | |||||
| this.paragraphRef.nativeElement.textContent = row[this.displayedDataField]; | |||||
| } | |||||
| this.searchBoxFilled = true; | |||||
| this.searchBoxOpen = false; | |||||
| } | |||||
| } | |||||
| openSearchBox() { | |||||
| this.searchBoxOpen = !this.searchBoxOpen; | |||||
| if (this.searchBoxOpen && !this.searchBoxInitialized) { | |||||
| this.listComponent.getData(); | |||||
| this.searchBoxInitialized = true; | |||||
| } | |||||
| } | |||||
| clearSearch() { | |||||
| this.paragraphRef.nativeElement.textContent = ''; | |||||
| this.searchBoxFilled = false; | |||||
| this.documentForm.get(this.formId)?.setValue(null); | |||||
| } | |||||
| public getPageIndex() { | |||||
| return this.listComponent.getPageIndex(); | |||||
| } | |||||
| public getPageSize() { | |||||
| return this.listComponent.getPageSize(); | |||||
| } | |||||
| onSortChange = (sortState: Sort) => { | |||||
| } | |||||
| public static getDefaultColDefAccountsSniping(subResource?: string): ListColDefinition[] { | |||||
| return [ | |||||
| ListComponent.getDefaultColPosition(), | |||||
| { | |||||
| name: 'profile', | |||||
| text: 'game_account.profile', | |||||
| type: ListComponent.COLUMN_TYPE_TEXT_BOLD, | |||||
| field: 'profile', | |||||
| sortable: true, | |||||
| //subResource: subResource, | |||||
| } as ListColDefinition, | |||||
| { | |||||
| name: 'credits', | |||||
| text: 'game_account.credits', | |||||
| type: ListComponent.COLUMN_TYPE_NUMBER_BOLD, | |||||
| field: 'credits', | |||||
| //subResource: subResource, | |||||
| sortable: true, | |||||
| } as ListColDefinition, | |||||
| { | |||||
| name: 'running', | |||||
| text: 'sniping.running', | |||||
| type: ListComponent.COLUMN_TYPE_BOOLEAN, | |||||
| field: 'running', | |||||
| sortable: true, | |||||
| filterType: FilterBarComponent.FILTER_TYPE_BOOLEAN, | |||||
| } as ListColDefinition, | |||||
| { | |||||
| name: 'blocked', | |||||
| text: 'sniping.blocked', | |||||
| type: ListComponent.COLUMN_TYPE_BOOLEAN, | |||||
| field: 'blocked', | |||||
| sortable: true, | |||||
| filterType: FilterBarComponent.FILTER_TYPE_BOOLEAN, | |||||
| } as ListColDefinition, | |||||
| { | |||||
| name: 'snipingCnt1h', | |||||
| text: 'sniping.snipingCnt1h', | |||||
| type: ListComponent.COLUMN_TYPE_NUMBER, | |||||
| field: 'snipingCnt1h', | |||||
| sortable: true, | |||||
| } as ListColDefinition, | |||||
| { | |||||
| name: 'snipingCnt3h', | |||||
| text: 'sniping.snipingCnt3h', | |||||
| type: ListComponent.COLUMN_TYPE_NUMBER, | |||||
| field: 'snipingCnt3h', | |||||
| sortable: true, | |||||
| } as ListColDefinition, | |||||
| { | |||||
| name: 'snipingCnt6h', | |||||
| text: 'sniping.snipingCnt6h', | |||||
| type: ListComponent.COLUMN_TYPE_NUMBER, | |||||
| field: 'snipingCnt6h', | |||||
| sortable: true, | |||||
| } as ListColDefinition, | |||||
| { | |||||
| name: 'snipingCnt12h', | |||||
| text: 'sniping.snipingCnt12h', | |||||
| type: ListComponent.COLUMN_TYPE_NUMBER, | |||||
| field: 'snipingCnt12h', | |||||
| sortable: true, | |||||
| } as ListColDefinition, | |||||
| { | |||||
| name: 'snipingCnt24h', | |||||
| text: 'sniping.snipingCnt24h', | |||||
| type: ListComponent.COLUMN_TYPE_NUMBER, | |||||
| field: 'snipingCnt24h', | |||||
| sortable: true, | |||||
| } as ListColDefinition, | |||||
| { | |||||
| name: 'snipingCnt3d', | |||||
| text: 'sniping.snipingCnt3d', | |||||
| type: ListComponent.COLUMN_TYPE_NUMBER, | |||||
| field: 'snipingCnt3d', | |||||
| sortable: true, | |||||
| } as ListColDefinition, | |||||
| { | |||||
| name: 'snipingCnt1w', | |||||
| text: 'sniping.snipingCnt1w', | |||||
| type: ListComponent.COLUMN_TYPE_NUMBER, | |||||
| field: 'snipingCnt1w', | |||||
| sortable: true, | |||||
| } as ListColDefinition, | |||||
| { | |||||
| name: 'snipingDate', | |||||
| text: 'game_account.snipingDate', | |||||
| type: ListComponent.COLUMN_TYPE_DATE, | |||||
| field: 'snipingDate', | |||||
| sortable: true, | |||||
| } as ListColDefinition, | |||||
| { | |||||
| name: 'relistDate', | |||||
| text: 'game_account.relistDate', | |||||
| type: ListComponent.COLUMN_TYPE_DATE, | |||||
| field: 'relistDate', | |||||
| sortable: true, | |||||
| } as ListColDefinition, | |||||
| ]; | |||||
| } | |||||
| public static getDefaultColDefCandidates(subResource?: string): ListColDefinition[] { | |||||
| return [ | |||||
| ListComponent.getDefaultColPosition(), | |||||
| { | |||||
| name: 'image', | |||||
| text: 'basic.image', | |||||
| type: ListComponent.COLUMN_TYPE_COMBINED_IMAGES, | |||||
| multipleFields: ['cardImageUrl', 'imageUrl', 'rating'], | |||||
| } as ListColDefinition, | |||||
| { | |||||
| name: 'firstname', | |||||
| text: 'candidate.firstname', | |||||
| type: ListComponent.COLUMN_TYPE_TEXT_BOLD, | |||||
| field: 'firstname', | |||||
| sortingSubResource: 'player', | |||||
| sortable: true, | |||||
| } as ListColDefinition, | |||||
| { | |||||
| name: 'lastname', | |||||
| text: 'candidate.lastname', | |||||
| type: ListComponent.COLUMN_TYPE_TEXT_BOLD, | |||||
| field: 'lastname', | |||||
| sortingSubResource: 'player', | |||||
| sortable: true, | |||||
| } as ListColDefinition, | |||||
| { | |||||
| name: 'nickname', | |||||
| text: 'candidate.nickname', | |||||
| type: ListComponent.COLUMN_TYPE_TEXT_BOLD, | |||||
| field: 'nickname', | |||||
| sortingSubResource: 'player', | |||||
| sortable: true, | |||||
| } as ListColDefinition, | |||||
| { | |||||
| name: 'rating', | |||||
| text: 'candidate.rating', | |||||
| type: ListComponent.COLUMN_TYPE_TEXT_BOLD, | |||||
| field: 'rating', | |||||
| sortable: true, | |||||
| } as ListColDefinition, | |||||
| { | |||||
| name: 'rarityName', | |||||
| text: 'candidate.rarityName', | |||||
| type: ListComponent.COLUMN_TYPE_TEXT, | |||||
| field: 'name', | |||||
| subResource: 'rarity', | |||||
| sortingSubResource: 'rarity', | |||||
| sortable: true, | |||||
| } as ListColDefinition, | |||||
| ]; | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,9 @@ | |||||
| <div class="toggle-component" (click)="openToggle()"> | |||||
| <div class="toggle collapsed" [ngClass]="{'small': small}" data-bs-toggle="collapse" [attr.data-bs-target]="'#collapse-' + toggleId" | |||||
| aria-expanded="false"> | |||||
| <p>{{headline | translate}}<span *ngIf="activeFilterCount > 0" class="active-filter-count"> ( {{ activeFilterCount }} )</span></p> | |||||
| </div> | |||||
| <div class="collapse" id="collapse-{{ toggleId }}"> | |||||
| <ng-content></ng-content> | |||||
| </div> | |||||
| </div> | |||||
| @@ -0,0 +1,23 @@ | |||||
| import { ComponentFixture, TestBed } from '@angular/core/testing'; | |||||
| import { ToggleComponent } from './toggle.component'; | |||||
| describe('ToggleComponent', () => { | |||||
| let component: ToggleComponent; | |||||
| let fixture: ComponentFixture<ToggleComponent>; | |||||
| beforeEach(async () => { | |||||
| await TestBed.configureTestingModule({ | |||||
| declarations: [ToggleComponent] | |||||
| }) | |||||
| .compileComponents(); | |||||
| fixture = TestBed.createComponent(ToggleComponent); | |||||
| component = fixture.componentInstance; | |||||
| fixture.detectChanges(); | |||||
| }); | |||||
| it('should create', () => { | |||||
| expect(component).toBeTruthy(); | |||||
| }); | |||||
| }); | |||||
| @@ -0,0 +1,23 @@ | |||||
| import {Component, Input} from '@angular/core'; | |||||
| import { v4 as uuidv4 } from 'uuid'; | |||||
| @Component({ | |||||
| selector: 'app-toggle', | |||||
| templateUrl: './toggle.component.html', | |||||
| styleUrl: './toggle.component.scss' | |||||
| }) | |||||
| export class ToggleComponent { | |||||
| @Input() public headline!: string; | |||||
| @Input() small: boolean = false; | |||||
| @Input() activeFilterCount: number = 0; | |||||
| public isOpened: boolean = false; | |||||
| protected toggleId: string; | |||||
| constructor() { | |||||
| this.toggleId = uuidv4(); | |||||
| } | |||||
| openToggle() { | |||||
| this.isOpened = true; | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,620 @@ | |||||
| import { FormGroup, FormControl, Validators } from '@angular/forms'; | |||||
| export const accountTradePileItemForm = new FormGroup({ | |||||
| dbId: new FormControl(null, []), | |||||
| account: new FormControl(null, []), | |||||
| candidateItem: new FormControl(null, []), | |||||
| eaId: new FormControl(null, []), | |||||
| eaAssetId: new FormControl(null, []), | |||||
| eaResourceId: new FormControl(null, []), | |||||
| rareFlag: new FormControl(null, []), | |||||
| itemType: new FormControl(null, []), | |||||
| rating: new FormControl(null, []), | |||||
| contracts: new FormControl(null, []), | |||||
| playStyle: new FormControl(null, []), | |||||
| startingBid: new FormControl(null, []), | |||||
| binPrice: new FormControl(null, []), | |||||
| individualPrice: new FormControl(null, []), | |||||
| minRange: new FormControl(null, []), | |||||
| maxRange: new FormControl(null, []), | |||||
| lastSalePrice: new FormControl(null, []), | |||||
| tradeState: new FormControl(null, []), | |||||
| eaTradeId: new FormControl(null, []), | |||||
| rebuy: new FormControl(null, []), | |||||
| leagueId: new FormControl(null, []), | |||||
| teamId: new FormControl(null, []), | |||||
| nationId: new FormControl(null, []), | |||||
| listCnt: new FormControl(null, []), | |||||
| openBidCnt: new FormControl(null, []), | |||||
| snipedItem: new FormControl(null, []), | |||||
| marketAverage: new FormControl(null, []), | |||||
| creationDate: new FormControl(null, []) | |||||
| }); | |||||
| export const accountTradePileItemJsonldForm = new FormGroup({ | |||||
| dbId: new FormControl(null, []), | |||||
| account: new FormControl(null, []), | |||||
| candidateItem: new FormControl(null, []), | |||||
| eaId: new FormControl(null, []), | |||||
| eaAssetId: new FormControl(null, []), | |||||
| eaResourceId: new FormControl(null, []), | |||||
| rareFlag: new FormControl(null, []), | |||||
| itemType: new FormControl(null, []), | |||||
| rating: new FormControl(null, []), | |||||
| contracts: new FormControl(null, []), | |||||
| playStyle: new FormControl(null, []), | |||||
| startingBid: new FormControl(null, []), | |||||
| binPrice: new FormControl(null, []), | |||||
| individualPrice: new FormControl(null, []), | |||||
| minRange: new FormControl(null, []), | |||||
| maxRange: new FormControl(null, []), | |||||
| lastSalePrice: new FormControl(null, []), | |||||
| tradeState: new FormControl(null, []), | |||||
| eaTradeId: new FormControl(null, []), | |||||
| rebuy: new FormControl(null, []), | |||||
| leagueId: new FormControl(null, []), | |||||
| teamId: new FormControl(null, []), | |||||
| nationId: new FormControl(null, []), | |||||
| listCnt: new FormControl(null, []), | |||||
| openBidCnt: new FormControl(null, []), | |||||
| snipedItem: new FormControl(null, []), | |||||
| marketAverage: new FormControl(null, []), | |||||
| creationDate: new FormControl(null, []) | |||||
| }); | |||||
| export const candidateForm = new FormGroup({ | |||||
| dbId: new FormControl(null, []), | |||||
| rarity: new FormControl(null, []), | |||||
| candidateStat: new FormControl(null, []), | |||||
| player: new FormControl(null, []), | |||||
| firstname: new FormControl(null, []), | |||||
| lastname: new FormControl(null, []), | |||||
| nickname: new FormControl(null, []), | |||||
| fullDisplayInfo: new FormControl(null, []), | |||||
| eaAssetId: new FormControl(null, [Validators.required]), | |||||
| eaResourceId: new FormControl(null, [Validators.required]), | |||||
| rareFlag: new FormControl(null, [Validators.required]), | |||||
| rarityName: new FormControl(null, []), | |||||
| image: new FormControl(null, []), | |||||
| imageUrl: new FormControl(null, []), | |||||
| cardImageUrl: new FormControl(null, []), | |||||
| stockCountTotal: new FormControl(null, []), | |||||
| stockCountReal: new FormControl(null, []), | |||||
| futBinId: new FormControl(null, []), | |||||
| futBinName: new FormControl(null, []), | |||||
| futBinPrice: new FormControl(null, []), | |||||
| futBinSellingPrice: new FormControl(null, []), | |||||
| lastFutBinUpdate: new FormControl(null, []), | |||||
| futWizId: new FormControl(null, []), | |||||
| futWizPrice: new FormControl(null, []), | |||||
| futwizName: new FormControl(null, []), | |||||
| futWizSellingPrice: new FormControl(null, []), | |||||
| lastFutWizUpdate: new FormControl(null, []), | |||||
| rating: new FormControl(null, [Validators.required]), | |||||
| highestBuyBinPrice: new FormControl(null, []), | |||||
| sellStartingBid: new FormControl(null, []), | |||||
| sellBinPrice: new FormControl(null, []), | |||||
| lastFoundMinRange: new FormControl(null, []), | |||||
| lastFoundMaxRange: new FormControl(null, []), | |||||
| lastFoundLowestBin: new FormControl(null, []), | |||||
| lowestBinUpdateDate: new FormControl(null, []), | |||||
| buy: new FormControl(null, [Validators.required]), | |||||
| maxBuyPrice: new FormControl(null, []), | |||||
| buyStyle: new FormControl(null, [Validators.required]), | |||||
| newBuySelective: new FormControl(null, [Validators.required]), | |||||
| remove: new FormControl(null, [Validators.required]), | |||||
| leagueId: new FormControl(null, []), | |||||
| nationId: new FormControl(null, []), | |||||
| prio: new FormControl(null, []), | |||||
| listCnt: new FormControl(null, [Validators.required]), | |||||
| soldCnt: new FormControl(null, [Validators.required]), | |||||
| note: new FormControl(null, []), | |||||
| relevant: new FormControl(null, [Validators.required]), | |||||
| adjust100: new FormControl(null, [Validators.required]), | |||||
| directReBuy: new FormControl(null, [Validators.required]), | |||||
| marketAverage: new FormControl(null, []), | |||||
| creationDate: new FormControl(null, [Validators.required]) | |||||
| }); | |||||
| export const candidateJsonldForm = new FormGroup({ | |||||
| dbId: new FormControl(null, []), | |||||
| rarity: new FormControl(null, []), | |||||
| candidateStat: new FormControl(null, []), | |||||
| player: new FormControl(null, []), | |||||
| firstname: new FormControl(null, []), | |||||
| lastname: new FormControl(null, []), | |||||
| nickname: new FormControl(null, []), | |||||
| fullDisplayInfo: new FormControl(null, []), | |||||
| eaAssetId: new FormControl(null, [Validators.required]), | |||||
| eaResourceId: new FormControl(null, [Validators.required]), | |||||
| rareFlag: new FormControl(null, [Validators.required]), | |||||
| rarityName: new FormControl(null, []), | |||||
| image: new FormControl(null, []), | |||||
| imageUrl: new FormControl(null, []), | |||||
| cardImageUrl: new FormControl(null, []), | |||||
| stockCountTotal: new FormControl(null, []), | |||||
| stockCountReal: new FormControl(null, []), | |||||
| futBinId: new FormControl(null, []), | |||||
| futBinName: new FormControl(null, []), | |||||
| futBinPrice: new FormControl(null, []), | |||||
| futBinSellingPrice: new FormControl(null, []), | |||||
| lastFutBinUpdate: new FormControl(null, []), | |||||
| futWizId: new FormControl(null, []), | |||||
| futWizPrice: new FormControl(null, []), | |||||
| futwizName: new FormControl(null, []), | |||||
| futWizSellingPrice: new FormControl(null, []), | |||||
| lastFutWizUpdate: new FormControl(null, []), | |||||
| rating: new FormControl(null, [Validators.required]), | |||||
| highestBuyBinPrice: new FormControl(null, []), | |||||
| sellStartingBid: new FormControl(null, []), | |||||
| sellBinPrice: new FormControl(null, []), | |||||
| lastFoundMinRange: new FormControl(null, []), | |||||
| lastFoundMaxRange: new FormControl(null, []), | |||||
| lastFoundLowestBin: new FormControl(null, []), | |||||
| lowestBinUpdateDate: new FormControl(null, []), | |||||
| buy: new FormControl(null, [Validators.required]), | |||||
| maxBuyPrice: new FormControl(null, []), | |||||
| buyStyle: new FormControl(null, [Validators.required]), | |||||
| newBuySelective: new FormControl(null, [Validators.required]), | |||||
| remove: new FormControl(null, [Validators.required]), | |||||
| leagueId: new FormControl(null, []), | |||||
| nationId: new FormControl(null, []), | |||||
| prio: new FormControl(null, []), | |||||
| listCnt: new FormControl(null, [Validators.required]), | |||||
| soldCnt: new FormControl(null, [Validators.required]), | |||||
| note: new FormControl(null, []), | |||||
| relevant: new FormControl(null, [Validators.required]), | |||||
| adjust100: new FormControl(null, [Validators.required]), | |||||
| directReBuy: new FormControl(null, [Validators.required]), | |||||
| marketAverage: new FormControl(null, []), | |||||
| creationDate: new FormControl(null, [Validators.required]) | |||||
| }); | |||||
| export const candidateStatJsonldForm = new FormGroup({ | |||||
| candidateItem: new FormControl(null, []), | |||||
| revRl6: new FormControl(null, []), | |||||
| rl6: new FormControl(null, []), | |||||
| sold6: new FormControl(null, []), | |||||
| rat6: new FormControl(null, []), | |||||
| rev6: new FormControl(null, []), | |||||
| revRl12: new FormControl(null, []), | |||||
| rl12: new FormControl(null, []), | |||||
| sold12: new FormControl(null, []), | |||||
| rat12: new FormControl(null, []), | |||||
| rev12: new FormControl(null, []), | |||||
| revRl24: new FormControl(null, []), | |||||
| rl24: new FormControl(null, []), | |||||
| sold24: new FormControl(null, []), | |||||
| rat24: new FormControl(null, []), | |||||
| rev24: new FormControl(null, []), | |||||
| revRl3d: new FormControl(null, []), | |||||
| rl3d: new FormControl(null, []), | |||||
| sold3d: new FormControl(null, []), | |||||
| rat3d: new FormControl(null, []), | |||||
| rev3d: new FormControl(null, []), | |||||
| revRl1w: new FormControl(null, []), | |||||
| rl1w: new FormControl(null, []), | |||||
| sold1w: new FormControl(null, []), | |||||
| rat1w: new FormControl(null, []), | |||||
| rev1w: new FormControl(null, []), | |||||
| revRl2w: new FormControl(null, []), | |||||
| rl2w: new FormControl(null, []), | |||||
| sold2w: new FormControl(null, []), | |||||
| rat2w: new FormControl(null, []), | |||||
| rev2w: new FormControl(null, []), | |||||
| revRl3w: new FormControl(null, []), | |||||
| rl3w: new FormControl(null, []), | |||||
| sold3w: new FormControl(null, []), | |||||
| rat3w: new FormControl(null, []), | |||||
| rev3w: new FormControl(null, []), | |||||
| revRl4w: new FormControl(null, []), | |||||
| rl4w: new FormControl(null, []), | |||||
| sold4w: new FormControl(null, []), | |||||
| rat4w: new FormControl(null, []), | |||||
| rev4w: new FormControl(null, []), | |||||
| revRl: new FormControl(null, []), | |||||
| rl: new FormControl(null, []), | |||||
| sold: new FormControl(null, []), | |||||
| rat: new FormControl(null, []), | |||||
| rev: new FormControl(null, []), | |||||
| snipingRev: new FormControl(null, []), | |||||
| creationDate: new FormControl(null, []), | |||||
| lastUpdateDate: new FormControl(null, []) | |||||
| }); | |||||
| export const candidateStockAccountsJsonldForm = new FormGroup({ | |||||
| candidatesStockAccounts: new FormControl(null, []), | |||||
| candidatesMissingAccounts: new FormControl(null, []) | |||||
| }); | |||||
| export const configForm = new FormGroup({ | |||||
| systemActive: new FormControl(null, []), | |||||
| systemRunning: new FormControl(null, []), | |||||
| lastUpdateDate: new FormControl(null, []), | |||||
| lastCheckDate: new FormControl(null, []), | |||||
| processCnt: new FormControl(null, []), | |||||
| sleepHourStart: new FormControl(null, [ | |||||
| Validators.min(0), | |||||
| Validators.max(23) | |||||
| ]), | |||||
| sleepHourEnd: new FormControl(null, [Validators.min(0), Validators.max(23)]), | |||||
| checkMaxSales: new FormControl(null, []), | |||||
| numMaxSales: new FormControl(null, [Validators.min(0), Validators.max(20)]) | |||||
| }); | |||||
| export const configJsonldForm = new FormGroup({ | |||||
| systemActive: new FormControl(null, []), | |||||
| systemRunning: new FormControl(null, []), | |||||
| lastUpdateDate: new FormControl(null, []), | |||||
| lastCheckDate: new FormControl(null, []), | |||||
| processCnt: new FormControl(null, []), | |||||
| sleepHourStart: new FormControl(null, [ | |||||
| Validators.min(0), | |||||
| Validators.max(23) | |||||
| ]), | |||||
| sleepHourEnd: new FormControl(null, [Validators.min(0), Validators.max(23)]), | |||||
| checkMaxSales: new FormControl(null, []), | |||||
| numMaxSales: new FormControl(null, [Validators.min(0), Validators.max(20)]) | |||||
| }); | |||||
| export const gameAccountForm = new FormGroup({ | |||||
| dbId: new FormControl(null, []), | |||||
| email: new FormControl(null, [Validators.required, Validators.email]), | |||||
| profile: new FormControl(null, [Validators.required]), | |||||
| password: new FormControl(null, [Validators.required]), | |||||
| emailPw: new FormControl(null, []), | |||||
| credits: new FormControl(null, []), | |||||
| cntItems: new FormControl(null, []), | |||||
| cntSoldItems: new FormControl(null, []), | |||||
| cntInactiveItems: new FormControl(null, []), | |||||
| active: new FormControl(null, [Validators.required]), | |||||
| running: new FormControl(null, [Validators.required]), | |||||
| relist: new FormControl(null, [Validators.required]), | |||||
| relistDate: new FormControl(null, []), | |||||
| blocked: new FormControl(null, [Validators.required]), | |||||
| sniping: new FormControl(null, [Validators.required]), | |||||
| snipingDate: new FormControl(null, []), | |||||
| tmOpen: new FormControl(null, [Validators.required]), | |||||
| tmState: new FormControl(null, []), | |||||
| dead: new FormControl(null, [Validators.required]), | |||||
| lockedMsg: new FormControl(null, [Validators.required]), | |||||
| dynPrices: new FormControl(null, [Validators.required]), | |||||
| newBuy: new FormControl(null, [Validators.required]), | |||||
| newBuyDate: new FormControl(null, []), | |||||
| newBuySelective: new FormControl(null, [Validators.required]), | |||||
| reBuy: new FormControl(null, [Validators.required]), | |||||
| rebuyDate: new FormControl(null, []), | |||||
| connectionDate: new FormControl(null, []), | |||||
| importWatchlist: new FormControl(null, [Validators.required]), | |||||
| autoReBuy: new FormControl(null, [Validators.required]), | |||||
| directReBuy: new FormControl(null, [Validators.required]), | |||||
| itemMaxBuyPrice: new FormControl(null, [ | |||||
| Validators.min(0), | |||||
| Validators.max(15000000) | |||||
| ]), | |||||
| revenue: new FormControl(null, []), | |||||
| futWizValue: new FormControl(null, []), | |||||
| eaMarketAvgValue: new FormControl(null, []), | |||||
| mfaCode: new FormControl(null, []), | |||||
| twoFactorAuthKey: new FormControl(null, []), | |||||
| login2FaViaApp: new FormControl(null, []), | |||||
| eaCode1: new FormControl(null, []), | |||||
| eaCode2: new FormControl(null, []), | |||||
| eaCode3: new FormControl(null, []), | |||||
| eaCode4: new FormControl(null, []), | |||||
| eaCode5: new FormControl(null, []), | |||||
| eaCode6: new FormControl(null, []), | |||||
| snipingCnt1h: new FormControl(null, []), | |||||
| snipingCnt3h: new FormControl(null, []), | |||||
| snipingCnt6h: new FormControl(null, []), | |||||
| snipingCnt12h: new FormControl(null, []), | |||||
| snipingCnt24h: new FormControl(null, []), | |||||
| snipingCnt3d: new FormControl(null, []), | |||||
| snipingCnt1w: new FormControl(null, []), | |||||
| note: new FormControl(null, []), | |||||
| loopStartDate: new FormControl(null, []), | |||||
| loopFinishDate: new FormControl(null, []), | |||||
| creationDate: new FormControl(null, []), | |||||
| owner: new FormControl(null, []) | |||||
| }); | |||||
| export const gameAccountJsonldForm = new FormGroup({ | |||||
| dbId: new FormControl(null, []), | |||||
| email: new FormControl(null, [Validators.required, Validators.email]), | |||||
| profile: new FormControl(null, [Validators.required]), | |||||
| password: new FormControl(null, [Validators.required]), | |||||
| emailPw: new FormControl(null, []), | |||||
| credits: new FormControl(null, []), | |||||
| cntItems: new FormControl(null, []), | |||||
| cntSoldItems: new FormControl(null, []), | |||||
| cntInactiveItems: new FormControl(null, []), | |||||
| active: new FormControl(null, [Validators.required]), | |||||
| running: new FormControl(null, [Validators.required]), | |||||
| relist: new FormControl(null, [Validators.required]), | |||||
| relistDate: new FormControl(null, []), | |||||
| blocked: new FormControl(null, [Validators.required]), | |||||
| sniping: new FormControl(null, [Validators.required]), | |||||
| snipingDate: new FormControl(null, []), | |||||
| tmOpen: new FormControl(null, [Validators.required]), | |||||
| tmState: new FormControl(null, []), | |||||
| dead: new FormControl(null, [Validators.required]), | |||||
| lockedMsg: new FormControl(null, [Validators.required]), | |||||
| dynPrices: new FormControl(null, [Validators.required]), | |||||
| newBuy: new FormControl(null, [Validators.required]), | |||||
| newBuyDate: new FormControl(null, []), | |||||
| newBuySelective: new FormControl(null, [Validators.required]), | |||||
| reBuy: new FormControl(null, [Validators.required]), | |||||
| rebuyDate: new FormControl(null, []), | |||||
| connectionDate: new FormControl(null, []), | |||||
| importWatchlist: new FormControl(null, [Validators.required]), | |||||
| autoReBuy: new FormControl(null, [Validators.required]), | |||||
| directReBuy: new FormControl(null, [Validators.required]), | |||||
| itemMaxBuyPrice: new FormControl(null, [ | |||||
| Validators.min(0), | |||||
| Validators.max(15000000) | |||||
| ]), | |||||
| revenue: new FormControl(null, []), | |||||
| futWizValue: new FormControl(null, []), | |||||
| eaMarketAvgValue: new FormControl(null, []), | |||||
| mfaCode: new FormControl(null, []), | |||||
| twoFactorAuthKey: new FormControl(null, []), | |||||
| login2FaViaApp: new FormControl(null, []), | |||||
| eaCode1: new FormControl(null, []), | |||||
| eaCode2: new FormControl(null, []), | |||||
| eaCode3: new FormControl(null, []), | |||||
| eaCode4: new FormControl(null, []), | |||||
| eaCode5: new FormControl(null, []), | |||||
| eaCode6: new FormControl(null, []), | |||||
| snipingCnt1h: new FormControl(null, []), | |||||
| snipingCnt3h: new FormControl(null, []), | |||||
| snipingCnt6h: new FormControl(null, []), | |||||
| snipingCnt12h: new FormControl(null, []), | |||||
| snipingCnt24h: new FormControl(null, []), | |||||
| snipingCnt3d: new FormControl(null, []), | |||||
| snipingCnt1w: new FormControl(null, []), | |||||
| note: new FormControl(null, []), | |||||
| loopStartDate: new FormControl(null, []), | |||||
| loopFinishDate: new FormControl(null, []), | |||||
| creationDate: new FormControl(null, []), | |||||
| owner: new FormControl(null, []) | |||||
| }); | |||||
| export const logAccountCreditJsonldForm = new FormGroup({ | |||||
| gameAccount: new FormControl(null, []), | |||||
| credits: new FormControl(null, []), | |||||
| revenue: new FormControl(null, []), | |||||
| creationDate: new FormControl(null, []) | |||||
| }); | |||||
| export const logAccountProfitJsonldForm = new FormGroup({ | |||||
| gameAccount: new FormControl(null, []), | |||||
| credits: new FormControl(null, []), | |||||
| revenue: new FormControl(null, []), | |||||
| tpValue: new FormControl(null, []), | |||||
| revToday: new FormControl(null, []), | |||||
| rev3hours: new FormControl(null, []), | |||||
| rev6hours: new FormControl(null, []), | |||||
| rev12hours: new FormControl(null, []), | |||||
| rev24hours: new FormControl(null, []), | |||||
| rev3days: new FormControl(null, []), | |||||
| rev1week: new FormControl(null, []), | |||||
| rev2weeks: new FormControl(null, []), | |||||
| rev3weeks: new FormControl(null, []), | |||||
| rev4weeks: new FormControl(null, []), | |||||
| rev2months: new FormControl(null, []), | |||||
| rev3months: new FormControl(null, []), | |||||
| revTotal: new FormControl(null, []), | |||||
| numSalesToday: new FormControl(null, []), | |||||
| numSales3hours: new FormControl(null, []), | |||||
| numSales6hours: new FormControl(null, []), | |||||
| numSales12hours: new FormControl(null, []), | |||||
| numSales24hours: new FormControl(null, []), | |||||
| numSales3days: new FormControl(null, []), | |||||
| numSales1week: new FormControl(null, []), | |||||
| numSales2weeks: new FormControl(null, []), | |||||
| numSales3weeks: new FormControl(null, []), | |||||
| numSales4weeks: new FormControl(null, []), | |||||
| numSales2months: new FormControl(null, []), | |||||
| numSales3months: new FormControl(null, []), | |||||
| numSalesTotal: new FormControl(null, []), | |||||
| isDailyProfit: new FormControl(null, []), | |||||
| creationDate: new FormControl(null, []) | |||||
| }); | |||||
| export const logAccountSoldItemJsonldForm = new FormGroup({ | |||||
| dbId: new FormControl(null, []), | |||||
| gameAccount: new FormControl(null, []), | |||||
| candidateItem: new FormControl(null, []), | |||||
| eaId: new FormControl(null, []), | |||||
| contracts: new FormControl(null, []), | |||||
| playStyle: new FormControl(null, []), | |||||
| lastSalePrice: new FormControl(null, []), | |||||
| currentBid: new FormControl(null, []), | |||||
| startingBid: new FormControl(null, []), | |||||
| binPrice: new FormControl(null, []), | |||||
| minRange: new FormControl(null, []), | |||||
| maxRange: new FormControl(null, []), | |||||
| tradeState: new FormControl(null, []), | |||||
| eaTradeId: new FormControl(null, []), | |||||
| listCnt: new FormControl(null, []), | |||||
| revenue: new FormControl(null, []), | |||||
| reBought: new FormControl(null, []), | |||||
| snipedItem: new FormControl(null, []), | |||||
| firstListDate: new FormControl(null, []), | |||||
| creationDate: new FormControl(null, []) | |||||
| }); | |||||
| export const logGeneralJsonldForm = new FormGroup({ | |||||
| dbId: new FormControl(null, []), | |||||
| gameAccount: new FormControl(null, []), | |||||
| candidateItem: new FormControl(null, []), | |||||
| logType: new FormControl(null, [Validators.required]), | |||||
| message: new FormControl(null, []), | |||||
| creationDate: new FormControl(null, []) | |||||
| }); | |||||
| export const logTotalProfitForm = new FormGroup({ | |||||
| credits: new FormControl(null, []), | |||||
| revenue: new FormControl(null, []), | |||||
| tpValueFutwiz: new FormControl(null, []), | |||||
| tpValueEaAverage: new FormControl(null, []), | |||||
| revToday: new FormControl(null, []), | |||||
| rev3hours: new FormControl(null, []), | |||||
| rev6hours: new FormControl(null, []), | |||||
| rev12hours: new FormControl(null, []), | |||||
| rev24hours: new FormControl(null, []), | |||||
| rev3days: new FormControl(null, []), | |||||
| rev1week: new FormControl(null, []), | |||||
| rev2weeks: new FormControl(null, []), | |||||
| rev3weeks: new FormControl(null, []), | |||||
| rev4weeks: new FormControl(null, []), | |||||
| rev2months: new FormControl(null, []), | |||||
| rev3months: new FormControl(null, []), | |||||
| revTotal: new FormControl(null, []), | |||||
| numSalesToday: new FormControl(null, []), | |||||
| numSales3hours: new FormControl(null, []), | |||||
| numSales6hours: new FormControl(null, []), | |||||
| numSales12hours: new FormControl(null, []), | |||||
| numSales24hours: new FormControl(null, []), | |||||
| numSales3days: new FormControl(null, []), | |||||
| numSales1week: new FormControl(null, []), | |||||
| numSales2weeks: new FormControl(null, []), | |||||
| numSales3weeks: new FormControl(null, []), | |||||
| numSales4weeks: new FormControl(null, []), | |||||
| numSales2months: new FormControl(null, []), | |||||
| numSales3months: new FormControl(null, []), | |||||
| numSalesTotal: new FormControl(null, []), | |||||
| isDailyProfit: new FormControl(null, []), | |||||
| creationDate: new FormControl(null, []) | |||||
| }); | |||||
| export const logTotalProfitJsonldForm = new FormGroup({ | |||||
| credits: new FormControl(null, []), | |||||
| revenue: new FormControl(null, []), | |||||
| tpValueFutwiz: new FormControl(null, []), | |||||
| tpValueEaAverage: new FormControl(null, []), | |||||
| revToday: new FormControl(null, []), | |||||
| rev3hours: new FormControl(null, []), | |||||
| rev6hours: new FormControl(null, []), | |||||
| rev12hours: new FormControl(null, []), | |||||
| rev24hours: new FormControl(null, []), | |||||
| rev3days: new FormControl(null, []), | |||||
| rev1week: new FormControl(null, []), | |||||
| rev2weeks: new FormControl(null, []), | |||||
| rev3weeks: new FormControl(null, []), | |||||
| rev4weeks: new FormControl(null, []), | |||||
| rev2months: new FormControl(null, []), | |||||
| rev3months: new FormControl(null, []), | |||||
| revTotal: new FormControl(null, []), | |||||
| numSalesToday: new FormControl(null, []), | |||||
| numSales3hours: new FormControl(null, []), | |||||
| numSales6hours: new FormControl(null, []), | |||||
| numSales12hours: new FormControl(null, []), | |||||
| numSales24hours: new FormControl(null, []), | |||||
| numSales3days: new FormControl(null, []), | |||||
| numSales1week: new FormControl(null, []), | |||||
| numSales2weeks: new FormControl(null, []), | |||||
| numSales3weeks: new FormControl(null, []), | |||||
| numSales4weeks: new FormControl(null, []), | |||||
| numSales2months: new FormControl(null, []), | |||||
| numSales3months: new FormControl(null, []), | |||||
| numSalesTotal: new FormControl(null, []), | |||||
| isDailyProfit: new FormControl(null, []), | |||||
| creationDate: new FormControl(null, []) | |||||
| }); | |||||
| export const mediaObjectJsonldMediaObjectReadForm = new FormGroup({ | |||||
| contentUrl: new FormControl(null, []) | |||||
| }); | |||||
| export const modeConfigForm = new FormGroup({ | |||||
| autoReBuyMinSoldItems: new FormControl(null, [ | |||||
| Validators.min(0), | |||||
| Validators.max(20) | |||||
| ]), | |||||
| autoReBuyMinLastHours: new FormControl(null, [ | |||||
| Validators.min(0), | |||||
| Validators.max(20) | |||||
| ]) | |||||
| }); | |||||
| export const modeConfigJsonldForm = new FormGroup({ | |||||
| autoReBuyMinSoldItems: new FormControl(null, [ | |||||
| Validators.min(0), | |||||
| Validators.max(20) | |||||
| ]), | |||||
| autoReBuyMinLastHours: new FormControl(null, [ | |||||
| Validators.min(0), | |||||
| Validators.max(20) | |||||
| ]) | |||||
| }); | |||||
| export const playerForm = new FormGroup({ | |||||
| eaAssetId: new FormControl(null, []), | |||||
| firstname: new FormControl(null, []), | |||||
| lastname: new FormControl(null, []), | |||||
| nickname: new FormControl(null, []), | |||||
| rating: new FormControl(null, []), | |||||
| creationDate: new FormControl(null, []) | |||||
| }); | |||||
| export const playerJsonldForm = new FormGroup({ | |||||
| eaAssetId: new FormControl(null, []), | |||||
| firstname: new FormControl(null, []), | |||||
| lastname: new FormControl(null, []), | |||||
| nickname: new FormControl(null, []), | |||||
| rating: new FormControl(null, []), | |||||
| creationDate: new FormControl(null, []) | |||||
| }); | |||||
| export const rarityForm = new FormGroup({ | |||||
| rareFlag: new FormControl(null, []), | |||||
| name: new FormControl(null, []), | |||||
| untradable: new FormControl(null, []), | |||||
| image: new FormControl(null, []), | |||||
| imageUrl: new FormControl(null, []), | |||||
| imageBronze: new FormControl(null, []) | |||||
| }); | |||||
| export const rarityJsonldForm = new FormGroup({ | |||||
| rareFlag: new FormControl(null, []), | |||||
| name: new FormControl(null, []), | |||||
| untradable: new FormControl(null, []), | |||||
| image: new FormControl(null, []), | |||||
| imageUrl: new FormControl(null, []), | |||||
| imageBronze: new FormControl(null, []) | |||||
| }); | |||||
| export const systemStatJsonldForm = new FormGroup({ | |||||
| totalLogProfit: new FormControl(null, []), | |||||
| config: new FormControl(null, []), | |||||
| modeConfig: new FormControl(null, []), | |||||
| numAccounts: new FormControl(null, []), | |||||
| numDeadAccounts: new FormControl(null, []), | |||||
| numActiveAccounts: new FormControl(null, []), | |||||
| numTmOpenAccounts: new FormControl(null, []), | |||||
| numTmClosedAccounts: new FormControl(null, []), | |||||
| numBlockedAccounts: new FormControl(null, []), | |||||
| numRunningAccounts: new FormControl(null, []), | |||||
| numTradepileItems: new FormControl(null, []), | |||||
| numSoldTradepileItems: new FormControl(null, []), | |||||
| numActiveTradepileItems: new FormControl(null, []), | |||||
| numExpiredTradepileItems: new FormControl(null, []), | |||||
| numInactiveTradepileItems: new FormControl(null, []), | |||||
| numCandidates: new FormControl(null, []), | |||||
| numRelevantCandidates: new FormControl(null, []), | |||||
| numBuyCandidates: new FormControl(null, []), | |||||
| totalSnipingRev: new FormControl(null, []) | |||||
| }); | |||||
| export const userJsonldForm = new FormGroup({ | |||||
| email: new FormControl(null, [Validators.required, Validators.email]), | |||||
| firstName: new FormControl(null, [Validators.required]), | |||||
| lastName: new FormControl(null, [Validators.required]), | |||||
| image: new FormControl(null, []), | |||||
| imageUrl: new FormControl(null, []), | |||||
| fullName: new FormControl(null, []), | |||||
| password: new FormControl(null, []), | |||||
| active: new FormControl(null, []), | |||||
| createdAt: new FormControl(null, []) | |||||
| }); | |||||
| @@ -0,0 +1,17 @@ | |||||
| import { TestBed } from '@angular/core/testing'; | |||||
| import { CanActivateFn } from '@angular/router'; | |||||
| import { adminGuard } from './admin.guard'; | |||||
| describe('adminGuard', () => { | |||||
| const executeGuard: CanActivateFn = (...guardParameters) => | |||||
| TestBed.runInInjectionContext(() => adminGuard(...guardParameters)); | |||||
| beforeEach(() => { | |||||
| TestBed.configureTestingModule({}); | |||||
| }); | |||||
| it('should be created', () => { | |||||
| expect(executeGuard).toBeTruthy(); | |||||
| }); | |||||
| }); | |||||
| @@ -0,0 +1,12 @@ | |||||
| import {CanActivateFn, Router} from '@angular/router'; | |||||
| import {inject} from "@angular/core"; | |||||
| import {AccountService} from "@app/_services"; | |||||
| export const adminGuard: CanActivateFn = (route, state) => { | |||||
| const accountService = inject(AccountService); | |||||
| if (accountService.isLoggedIn() && accountService.isUserAdmin()) { | |||||
| return true; | |||||
| } | |||||
| inject(Router).navigate(['/account/login'], { queryParams: { returnUrl: state.url }}); | |||||
| return false; | |||||
| }; | |||||
| @@ -0,0 +1,17 @@ | |||||
| import { TestBed } from '@angular/core/testing'; | |||||
| import { CanActivateFn } from '@angular/router'; | |||||
| import { gameAccountOwnerGuard } from './game-account-owner.guard'; | |||||
| describe('gameAccountOwnerGuard', () => { | |||||
| const executeGuard: CanActivateFn = (...guardParameters) => | |||||
| TestBed.runInInjectionContext(() => gameAccountOwnerGuard(...guardParameters)); | |||||
| beforeEach(() => { | |||||
| TestBed.configureTestingModule({}); | |||||
| }); | |||||
| it('should be created', () => { | |||||
| expect(executeGuard).toBeTruthy(); | |||||
| }); | |||||
| }); | |||||
| @@ -0,0 +1,27 @@ | |||||
| import { CanActivateFn, Router } from '@angular/router'; | |||||
| import { inject } from "@angular/core"; | |||||
| import { map } from 'rxjs/operators'; | |||||
| import {GameAccountService} from "@app/core/api/v1"; | |||||
| export const gameAccountOwnerGuard: CanActivateFn = (route, state) => { | |||||
| const gameAccountService = inject(GameAccountService); | |||||
| const router = inject(Router); | |||||
| const gameAccountId = route.paramMap.get('id'); | |||||
| if (!gameAccountId) { | |||||
| router.navigate(['/error']); // Oder eine andere geeignete Route | |||||
| return false; | |||||
| } | |||||
| return gameAccountService.gameAccountsIdGet(gameAccountId).pipe( | |||||
| map(gameAccount => { | |||||
| return true; | |||||
| // if (isOwner) { | |||||
| // return true; | |||||
| // } else { | |||||
| // router.navigate(['/' + ROUTE_DASHBOARD]); // Oder eine andere geeignete Route | |||||
| // return false; | |||||
| // } | |||||
| }) | |||||
| ); | |||||
| }; | |||||
| @@ -0,0 +1,17 @@ | |||||
| import { TestBed } from '@angular/core/testing'; | |||||
| import { CanActivateFn } from '@angular/router'; | |||||
| import { salesGuard } from './sales.guard'; | |||||
| describe('salesGuard', () => { | |||||
| const executeGuard: CanActivateFn = (...guardParameters) => | |||||
| TestBed.runInInjectionContext(() => salesGuard(...guardParameters)); | |||||
| beforeEach(() => { | |||||
| TestBed.configureTestingModule({}); | |||||
| }); | |||||
| it('should be created', () => { | |||||
| expect(executeGuard).toBeTruthy(); | |||||
| }); | |||||
| }); | |||||
| @@ -0,0 +1,13 @@ | |||||
| import {CanActivateFn, Router} from '@angular/router'; | |||||
| import {inject} from "@angular/core"; | |||||
| import {AccountService} from "@app/_services"; | |||||
| import {Role} from "@app/_helpers/role"; | |||||
| export const salesGuard: CanActivateFn = (route, state) => { | |||||
| const accountService = inject(AccountService); | |||||
| if (accountService.isLoggedIn() && accountService.userHasRole(Role.ROLE_ADMIN)) { | |||||
| return true; | |||||
| } | |||||
| inject(Router).navigate(['/account/login'], { queryParams: { returnUrl: state.url }}); | |||||
| return false; | |||||
| }; | |||||
| @@ -0,0 +1,17 @@ | |||||
| import { TestBed } from '@angular/core/testing'; | |||||
| import { CanActivateFn } from '@angular/router'; | |||||
| import { userGuard } from './user.guard'; | |||||
| describe('userGuardGuard', () => { | |||||
| const executeGuard: CanActivateFn = (...guardParameters) => | |||||
| TestBed.runInInjectionContext(() => userGuard(...guardParameters)); | |||||
| beforeEach(() => { | |||||
| TestBed.configureTestingModule({}); | |||||
| }); | |||||
| it('should be created', () => { | |||||
| expect(executeGuard).toBeTruthy(); | |||||
| }); | |||||
| }); | |||||
| @@ -0,0 +1,13 @@ | |||||
| import {CanActivateFn, Router} from '@angular/router'; | |||||
| import {inject} from "@angular/core"; | |||||
| import {AccountService} from "@app/_services"; | |||||
| import {Role} from "@app/_helpers/role"; | |||||
| export const userGuard: CanActivateFn = (route, state) => { | |||||
| const accountService = inject(AccountService); | |||||
| if (accountService.isLoggedIn() && accountService.userHasRole(Role.ROLE_USER)) { | |||||
| return true; | |||||
| } | |||||
| inject(Router).navigate(['/account/login'], { queryParams: { returnUrl: state.url }}); | |||||
| return false; | |||||
| }; | |||||
| @@ -0,0 +1,86 @@ | |||||
| import {DomSanitizer, SafeHtml} from "@angular/platform-browser"; | |||||
| import {Injectable} from "@angular/core"; | |||||
| import {NgbModal, NgbModalOptions} from "@ng-bootstrap/ng-bootstrap"; | |||||
| import {ModalStatus} from "@app/_helpers/modal.states"; | |||||
| @Injectable({providedIn: 'root'}) | |||||
| export class AppHelperService { | |||||
| constructor( | |||||
| private sanitizer: DomSanitizer, | |||||
| private modalService: NgbModal, | |||||
| ) { | |||||
| } | |||||
| public extractId(iri: string | undefined | null): string { | |||||
| if (iri !== undefined && iri !== null) { | |||||
| const iriRegex = /\/(\d+)$/; | |||||
| const match = iri.match(iriRegex); | |||||
| if (match && match[1]) { | |||||
| return match[1]; | |||||
| } | |||||
| } | |||||
| return ""; | |||||
| } | |||||
| public convertDate(dateString: string | null, withTime = false) { | |||||
| // number 10 for input date (2024-03-15) | |||||
| // number 16 for input datetime-local (2024-04-28T03:22) | |||||
| if (dateString !== null) { | |||||
| const date = new Date(dateString); | |||||
| return date.toISOString().slice(0, withTime ? 16 : 10); | |||||
| } | |||||
| return ""; | |||||
| } | |||||
| public getSafeLongtext(longtext: any): SafeHtml { | |||||
| if (longtext) { | |||||
| return this.sanitizer.bypassSecurityTrustHtml(longtext.replace(/\n/g, '<br>')); | |||||
| } | |||||
| return false; | |||||
| } | |||||
| public getModalOptions(): NgbModalOptions { | |||||
| return {centered: true} as NgbModalOptions; | |||||
| } | |||||
| public openModal(component: any, data: any, callback?: (callbackParam?: any) => void, callbackParam?: any): Promise<ModalStatus> { | |||||
| const modalRef = this.modalService.open(component); | |||||
| for (const key in data) { | |||||
| modalRef.componentInstance[key] = data[key]; | |||||
| } | |||||
| return modalRef.componentInstance.submit.subscribe((modalStatus: ModalStatus) => { | |||||
| if (modalStatus === ModalStatus.Submitted) { | |||||
| modalRef.dismiss(); | |||||
| if (callback) { | |||||
| callback(callbackParam); | |||||
| } | |||||
| } | |||||
| }); | |||||
| } | |||||
| public assertType<T>(value: any, type: string): asserts value is T { | |||||
| if (typeof value !== type) { | |||||
| throw new Error(`Expected ${type} but received ${typeof value}`); | |||||
| } | |||||
| } | |||||
| public getResourceLink(element: any, subResource?: any): string | null { | |||||
| let resourceLink: string = '/'; | |||||
| element = subResource !== undefined ? element[subResource] : element; | |||||
| if (element === undefined) { | |||||
| return null; | |||||
| } | |||||
| return resourceLink + '/' + this.extractId(element['id']); | |||||
| } | |||||
| public getLink(element: any, url: any): string { | |||||
| return url + "/" + this.extractId(element['id']); | |||||
| } | |||||
| public dumpObject(obj: any): string { | |||||
| return JSON.stringify(obj, null, 2); // Das `null, 2` formatiert das JSON mit Einrückungen | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,38 @@ | |||||
| import { Injectable } from '@angular/core'; | |||||
| import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor, HttpResponse } from '@angular/common/http'; | |||||
| import { Observable, throwError } from 'rxjs'; | |||||
| import { catchError, tap } from 'rxjs/operators'; | |||||
| import { AccountService, AlertService } from '@app/_services'; | |||||
| @Injectable() | |||||
| export class ErrorInterceptor implements HttpInterceptor { | |||||
| constructor( | |||||
| private accountService: AccountService, | |||||
| private alertService: AlertService, | |||||
| ) {} | |||||
| intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { | |||||
| return next.handle(request).pipe( | |||||
| tap(evt => { | |||||
| if (evt instanceof HttpResponse) { | |||||
| if ((request.method === 'POST' || request.method === 'PATCH') && evt.status === 200) { | |||||
| // Erfolgsmeldung für POST und PATCH | |||||
| this.alertService.success('Saved', {autoClose: true}); | |||||
| } | |||||
| } | |||||
| }), | |||||
| catchError(err => { | |||||
| if ([401, 403].includes(err.status) && this.accountService.userValue) { | |||||
| // auto logout if 401 or 403 response returned from api | |||||
| this.accountService.logout(); | |||||
| } | |||||
| console.log(err); | |||||
| this.alertService.error(err.message + ' - ' + err.error); | |||||
| const error = err.error?.message || err.statusText; | |||||
| return throwError(() => error); | |||||
| }) | |||||
| ); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,12 @@ | |||||
| import {FormGroup} from "@angular/forms"; | |||||
| export class FormGroupInitializer { | |||||
| public static initFormGroup(formGroup: FormGroup, model: any) { | |||||
| for (const controlName in formGroup.controls) { | |||||
| if (formGroup.controls.hasOwnProperty(controlName)) { | |||||
| formGroup.patchValue({[controlName]: model[controlName] ?? null}); | |||||
| } | |||||
| } | |||||
| return formGroup; | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,2 @@ | |||||
| export * from './error.interceptor'; | |||||
| export * from './jwt.interceptor'; | |||||
| @@ -0,0 +1,27 @@ | |||||
| import { Injectable } from '@angular/core'; | |||||
| import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor } from '@angular/common/http'; | |||||
| import { Observable } from 'rxjs'; | |||||
| import { environment } from '@environments/environment'; | |||||
| import { AccountService } from '@app/_services'; | |||||
| @Injectable() | |||||
| export class JwtInterceptor implements HttpInterceptor { | |||||
| constructor(private accountService: AccountService) { } | |||||
| intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { | |||||
| // add auth header with jwt if user is logged in and request is to the api url | |||||
| const user = this.accountService.userValue; | |||||
| const isLoggedIn = user && user.token; | |||||
| const isApiUrl = request.url.startsWith(environment.apiUrl); | |||||
| if (isLoggedIn && isApiUrl) { | |||||
| request = request.clone({ | |||||
| setHeaders: { | |||||
| Authorization: `Bearer ${user.token}` | |||||
| } | |||||
| }); | |||||
| } | |||||
| return next.handle(request); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,21 @@ | |||||
| import { Injectable } from '@angular/core'; | |||||
| import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor } from '@angular/common/http'; | |||||
| import {finalize, Observable} from 'rxjs'; | |||||
| import {LoadingService} from "@app/_services/loading.service"; | |||||
| @Injectable() | |||||
| export class LoadingInterceptor implements HttpInterceptor { | |||||
| constructor( | |||||
| private loadingService: LoadingService | |||||
| ) { } | |||||
| intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { | |||||
| this.loadingService.setLoading(true); | |||||
| return next.handle(request).pipe( | |||||
| finalize(() => { | |||||
| this.loadingService.setLoading(false); | |||||
| }) | |||||
| ); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,4 @@ | |||||
| export enum ModalStatus { | |||||
| Cancelled = 'Cancelled', | |||||
| Submitted = 'Submitted' | |||||
| } | |||||
| @@ -0,0 +1,116 @@ | |||||
| import { Injectable } from '@angular/core'; | |||||
| import {FormGroup} from "@angular/forms"; | |||||
| import {TranslateService} from "@ngx-translate/core"; | |||||
| import {PriceError} from "@app/_models/priceError"; | |||||
| @Injectable({ | |||||
| providedIn: 'root' | |||||
| }) | |||||
| export class PriceCalculatorService { | |||||
| constructor( | |||||
| protected translateService: TranslateService, | |||||
| ) { | |||||
| } | |||||
| public getCorrectPrice(event: FocusEvent, form: FormGroup) { | |||||
| const eventElement = event.target as HTMLInputElement; | |||||
| if (form.get(eventElement.id)?.value !== null && form.get(eventElement.id)?.value !== '') { | |||||
| let validPrice = this.calculateValidPrice(Number(eventElement.value)); | |||||
| form.get(eventElement.id)?.setValue(validPrice); | |||||
| } | |||||
| } | |||||
| public calculateValidPrice(price: number): number { | |||||
| price = Math.floor(price); | |||||
| if (price < 150) return 150; | |||||
| if (price < 1000) return price - (price % 50); | |||||
| if (price < 10000) return price - (price % 100); | |||||
| if (price < 50000) return price - (price % 250); | |||||
| if (price < 100000) return price - (price % 500); | |||||
| return price - (price % 1000); | |||||
| } | |||||
| public checkPriceConstellation( | |||||
| sellStartingBid: number | string, | |||||
| sellPriceBin: number | string, | |||||
| lastFoundMinRange: number | string | undefined, | |||||
| lastFoundMaxRange: number | string | undefined, | |||||
| ): PriceError { | |||||
| let res = { | |||||
| message: '', | |||||
| error: false | |||||
| } as PriceError; | |||||
| if ((sellStartingBid !== null && sellStartingBid !== '') && (sellPriceBin === null || sellPriceBin === '')) { | |||||
| res.error = true; | |||||
| res.message = this.translateService.instant('errors.sellPriceBinSellStartingBid'); | |||||
| return res; | |||||
| } | |||||
| if ((sellPriceBin !== null && sellPriceBin !== '') && (sellStartingBid === null || sellStartingBid === '')) { | |||||
| res.error = true; | |||||
| res.message = this.translateService.instant('errors.sellStartingBidSellPriceBin'); | |||||
| return res; | |||||
| } | |||||
| if (sellPriceBin <= sellStartingBid && (sellStartingBid !== null && sellStartingBid !== '') && (sellPriceBin !== null && sellPriceBin !== '')) { | |||||
| res.error = true; | |||||
| res.message = this.translateService.instant('errors.sellPriceBinSmallerSellStartingBid'); | |||||
| return res; | |||||
| } | |||||
| if (sellStartingBid >= sellPriceBin && (sellStartingBid !== null && sellStartingBid !== '') && (sellPriceBin !== null && sellPriceBin !== '')) { | |||||
| res.error = true; | |||||
| res.message = this.translateService.instant('errors.sellStartingBidLargerSellPriceBin'); | |||||
| return res; | |||||
| } | |||||
| if (lastFoundMinRange) { | |||||
| if (sellStartingBid < lastFoundMinRange && (sellStartingBid !== null && sellStartingBid !== '')) { | |||||
| res.error = true; | |||||
| res.message = this.translateService.instant('errors.sellStartingBidSmallerLastFoundMinRange'); | |||||
| return res; | |||||
| } | |||||
| } | |||||
| if (lastFoundMaxRange) { | |||||
| if (sellPriceBin > lastFoundMaxRange && (sellPriceBin !== null && sellPriceBin !== '')) { | |||||
| res.error = true; | |||||
| res.message = this.translateService.instant('errors.sellPriceBinLargerLastFoundMaxRange'); | |||||
| return res; | |||||
| } | |||||
| } | |||||
| if (lastFoundMinRange && lastFoundMaxRange) { | |||||
| if (lastFoundMaxRange <= lastFoundMinRange) { | |||||
| res.error = true; | |||||
| res.message = this.translateService.instant('errors.sellStartingBidSellPriceBin'); | |||||
| return res; | |||||
| } | |||||
| if (lastFoundMinRange >= lastFoundMaxRange) { | |||||
| res.error = true; | |||||
| res.message = this.translateService.instant('errors.lastFoundMinRangeLargerLastFoundMaxRange'); | |||||
| return res; | |||||
| } | |||||
| if ((lastFoundMinRange !== '') && (lastFoundMaxRange === '')) { | |||||
| res.error = true; | |||||
| res.message = this.translateService.instant('errors.lastFoundMaxRangeLastFoundMinRange'); | |||||
| return res; | |||||
| } | |||||
| if ((lastFoundMaxRange !== null && lastFoundMaxRange !== '') && (lastFoundMinRange === '')) { | |||||
| res.error = true; | |||||
| res.message = this.translateService.instant('errors.lastFoundMinRangeLastFoundMaxRange'); | |||||
| return res; | |||||
| } | |||||
| } | |||||
| return res; | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,75 @@ | |||||
| import { Injectable } from '@angular/core'; | |||||
| import { | |||||
| HttpInterceptor, | |||||
| HttpRequest, | |||||
| HttpHandler, | |||||
| HttpEvent, HttpResponse, | |||||
| } from '@angular/common/http'; | |||||
| import { Observable } from 'rxjs'; | |||||
| import { map } from 'rxjs/operators'; | |||||
| @Injectable() | |||||
| export class PropertyInterceptor implements HttpInterceptor { | |||||
| intercept( | |||||
| request: HttpRequest<any>, | |||||
| next: HttpHandler | |||||
| ): Observable<HttpEvent<any>> { | |||||
| return next.handle(request).pipe( | |||||
| map((event) => { | |||||
| if (event instanceof HttpResponse && event.body) { | |||||
| let modifiedBody; | |||||
| if (Array.isArray(event.body)) { | |||||
| // Wenn es sich um ein Array von Ressourcen handelt | |||||
| modifiedBody = this.mapDataItems(event.body); | |||||
| } else if (event.body['hydra:member']) { | |||||
| // Wenn es sich um eine Ressourcenkollektion handelt | |||||
| modifiedBody = { | |||||
| ...event.body, | |||||
| 'hydra:member': this.mapDataItems( | |||||
| event.body['hydra:member'] | |||||
| ), | |||||
| }; | |||||
| } else { | |||||
| // Wenn es sich um eine einzelne Ressource handelt | |||||
| modifiedBody = this.mapDataItem(event.body); | |||||
| } | |||||
| return event.clone({ | |||||
| body: modifiedBody, | |||||
| }); | |||||
| } | |||||
| return event; | |||||
| }) | |||||
| ); | |||||
| } | |||||
| private mapDataItems(items: any[]): any[] { | |||||
| return items.map((item) => this.mapDataItem(item)); | |||||
| } | |||||
| private mapDataItem(item: any): any { | |||||
| if (item && typeof item === 'object') { | |||||
| // Wenn es ein Objekt ist, überprüfe rekursiv | |||||
| const mappedItem = { ...item }; | |||||
| for (const key in mappedItem) { | |||||
| if (mappedItem.hasOwnProperty(key) && key === '@id') { | |||||
| mappedItem['id'] = mappedItem[key]; | |||||
| delete mappedItem[key]; | |||||
| } else if (Array.isArray(mappedItem[key])) { | |||||
| // Wenn es sich um ein Array handelt, überprüfe rekursiv jedes Element | |||||
| mappedItem[key] = this.mapDataItems(mappedItem[key]); | |||||
| } else if (mappedItem[key] && typeof mappedItem[key] === 'object') { | |||||
| // Wenn es ein Objekt ist, überprüfe rekursiv | |||||
| mappedItem[key] = this.mapDataItem(mappedItem[key]); | |||||
| } | |||||
| } | |||||
| return mappedItem; | |||||
| } | |||||
| return item; | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,6 @@ | |||||
| export class Role { | |||||
| public static ROLE_ADMIN: string = 'ROLE_ADMIN'; | |||||
| public static ROLE_USER: string = 'ROLE_USER'; | |||||
| public static ROLE_SALES: string = 'ROLE_SALES'; | |||||
| } | |||||
| @@ -0,0 +1,25 @@ | |||||
| export class Alert { | |||||
| id?: string; | |||||
| type?: AlertType; | |||||
| message?: string; | |||||
| autoClose?: boolean; | |||||
| keepAfterRouteChange?: boolean; | |||||
| fade?: boolean; | |||||
| constructor(init?:Partial<Alert>) { | |||||
| Object.assign(this, init); | |||||
| } | |||||
| } | |||||
| export enum AlertType { | |||||
| Success, | |||||
| Error, | |||||
| Info, | |||||
| Warning | |||||
| } | |||||
| export class AlertOptions { | |||||
| id?: string; | |||||
| autoClose?: boolean; | |||||
| keepAfterRouteChange?: boolean; | |||||
| } | |||||
| @@ -0,0 +1,2 @@ | |||||
| export * from './alert'; | |||||
| export * from './user'; | |||||
| @@ -0,0 +1,6 @@ | |||||
| export type OrderFilter = 'asc' | 'desc' | undefined; | |||||
| export const OrderFilter = { | |||||
| Asc: 'asc' as OrderFilter, | |||||
| Desc: 'desc' as OrderFilter, | |||||
| Undefined: undefined as OrderFilter | |||||
| } | |||||
| @@ -0,0 +1,4 @@ | |||||
| export interface PriceError { | |||||
| message: string; | |||||
| error: boolean; | |||||
| } | |||||
| @@ -0,0 +1,11 @@ | |||||
| export interface SnipingResponse { | |||||
| messages: string[]; | |||||
| excludedAccountIds: number[], | |||||
| errorAccountIds: number[], | |||||
| snipingAccountIds: number[], | |||||
| abortSniping: boolean, | |||||
| boughtItems: number; | |||||
| priceCheckFoundItems: number; | |||||
| priceCheckFoundPrices: number[]; | |||||
| priceCheckMessages: string[]; | |||||
| } | |||||
| @@ -0,0 +1,12 @@ | |||||
| import {UserJsonld} from "@app/core/api/v1"; | |||||
| export class User { | |||||
| id?: string; | |||||
| email?: string; | |||||
| password?: string; | |||||
| firstName?: string; | |||||
| lastName?: string; | |||||
| roles?: string[]; | |||||
| token?: string; | |||||
| userResource?: UserJsonld; | |||||
| } | |||||
| @@ -0,0 +1,101 @@ | |||||
| import { Injectable } from '@angular/core'; | |||||
| import { Router } from '@angular/router'; | |||||
| import { HttpClient } from '@angular/common/http'; | |||||
| import { BehaviorSubject, Observable } from 'rxjs'; | |||||
| import { map } from 'rxjs/operators'; | |||||
| import { environment } from '@environments/environment'; | |||||
| import { User } from '@app/_models'; | |||||
| @Injectable({ providedIn: 'root' }) | |||||
| export class AccountService { | |||||
| private userSubject: BehaviorSubject<User | null>; | |||||
| public user: Observable<User | null>; | |||||
| constructor( | |||||
| private router: Router, | |||||
| private http: HttpClient | |||||
| ) { | |||||
| this.userSubject = new BehaviorSubject(JSON.parse(localStorage.getItem('user')!)); | |||||
| this.user = this.userSubject.asObservable(); | |||||
| } | |||||
| public get userValue() { | |||||
| return this.userSubject.value; | |||||
| } | |||||
| login(email: string, password: string) { | |||||
| return this.http.post<User>(`${environment.apiUrl}/auth`, { email, password }) | |||||
| .pipe(map(user => { | |||||
| // store user details and jwt token in local storage to keep user logged in between page refreshes | |||||
| localStorage.setItem('user', JSON.stringify(user)); | |||||
| this.userSubject.next(user); | |||||
| return user; | |||||
| })); | |||||
| } | |||||
| logout() { | |||||
| // remove user from local storage and set current user to null | |||||
| localStorage.removeItem('user'); | |||||
| this.userSubject.next(null); | |||||
| this.router.navigate(['/account/login']); | |||||
| } | |||||
| isLoggedIn() { | |||||
| return this.userValue !== null; | |||||
| } | |||||
| isUserAdmin(): boolean { | |||||
| if (this.userValue && this.userValue?.roles) { | |||||
| return this.userValue?.roles?.includes('ROLE_ADMIN'); | |||||
| } | |||||
| return false; | |||||
| } | |||||
| userHasRole(role: string): boolean { | |||||
| if (this.userValue && this.userValue?.roles) { | |||||
| return this.userValue?.roles?.includes(role); | |||||
| } | |||||
| return false; | |||||
| } | |||||
| register(user: User) { | |||||
| return this.http.post(`${environment.apiUrl}/users/register`, user); | |||||
| } | |||||
| getAll() { | |||||
| return this.http.get<User[]>(`${environment.apiUrl}/users`); | |||||
| } | |||||
| getById(id: string) { | |||||
| return this.http.get<User>(`${environment.apiUrl}/users/${id}`); | |||||
| } | |||||
| update(id: string, params: any) { | |||||
| return this.http.put(`${environment.apiUrl}/users/${id}`, params) | |||||
| .pipe(map(x => { | |||||
| // update stored user if the logged in user updated their own record | |||||
| if (id == this.userValue?.id) { | |||||
| // update local storage | |||||
| const user = { ...this.userValue, ...params }; | |||||
| localStorage.setItem('user', JSON.stringify(user)); | |||||
| // publish updated user to subscribers | |||||
| this.userSubject.next(user); | |||||
| } | |||||
| return x; | |||||
| })); | |||||
| } | |||||
| delete(id: string) { | |||||
| return this.http.delete(`${environment.apiUrl}/users/${id}`) | |||||
| .pipe(map(x => { | |||||
| // auto logout if the logged in user deleted their own record | |||||
| if (id == this.userValue?.id) { | |||||
| this.logout(); | |||||
| } | |||||
| return x; | |||||
| })); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,44 @@ | |||||
| import { Injectable } from '@angular/core'; | |||||
| import { Observable, Subject } from 'rxjs'; | |||||
| import { filter } from 'rxjs/operators'; | |||||
| import { Alert, AlertType, AlertOptions } from '@app/_models'; | |||||
| @Injectable({ providedIn: 'root' }) | |||||
| export class AlertService { | |||||
| private subject = new Subject<Alert>(); | |||||
| private defaultId = 'default-alert'; | |||||
| // enable subscribing to alerts observable | |||||
| onAlert(id = this.defaultId): Observable<Alert> { | |||||
| return this.subject.asObservable().pipe(filter(x => x && x.id === id)); | |||||
| } | |||||
| // convenience methods | |||||
| success(message: string, options?: AlertOptions) { | |||||
| this.alert(new Alert({ ...options, type: AlertType.Success, message })); | |||||
| } | |||||
| error(message: string, options?: AlertOptions) { | |||||
| this.alert(new Alert({ ...options, type: AlertType.Error, message })); | |||||
| } | |||||
| info(message: string, options?: AlertOptions) { | |||||
| this.alert(new Alert({ ...options, type: AlertType.Info, message })); | |||||
| } | |||||
| warn(message: string, options?: AlertOptions) { | |||||
| this.alert(new Alert({ ...options, type: AlertType.Warning, message })); | |||||
| } | |||||
| // main alert method | |||||
| alert(alert: Alert) { | |||||
| alert.id = alert.id || this.defaultId; | |||||
| this.subject.next(alert); | |||||
| } | |||||
| // clear alerts | |||||
| clear(id = this.defaultId) { | |||||
| this.subject.next(new Alert({ id })); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,65 @@ | |||||
| import { Injectable } from '@angular/core'; | |||||
| import {HttpClient, HttpHeaders} from "@angular/common/http"; | |||||
| import {User} from "@app/_models"; | |||||
| import {environment} from "@environments/environment"; | |||||
| @Injectable({ | |||||
| providedIn: 'root' | |||||
| }) | |||||
| export class DataImportService { | |||||
| constructor( | |||||
| private httpClient: HttpClient | |||||
| ) { | |||||
| } | |||||
| futwizImport( | |||||
| futwizCandidateUrl: string, | |||||
| futbinCandidateUrl: string, | |||||
| updateCandidate: boolean = false, | |||||
| buyCandidate: boolean = false, | |||||
| relevantCandidate: boolean = false, | |||||
| futwizCandidateHtml: string | null, | |||||
| bid: number | null, | |||||
| bin: number | null, | |||||
| optionalEaResourceId: number | null, | |||||
| ) { | |||||
| const formData = new FormData(); | |||||
| formData.append('futwizCandidateUrl', futwizCandidateUrl); | |||||
| formData.append('futbinCandidateUrl', futbinCandidateUrl); | |||||
| formData.append('updateCandidate', updateCandidate ? '1' : '0'); | |||||
| formData.append('buyCandidate', buyCandidate ? '1' : '0'); | |||||
| formData.append('relevantCandidate', relevantCandidate ? '1' : '0'); | |||||
| if (futwizCandidateHtml !== null && futwizCandidateHtml !== "") { | |||||
| formData.append('futwizCandidateHtml', futwizCandidateHtml); | |||||
| } | |||||
| if (bid !== null) { | |||||
| formData.append('bid', bid.toString()); | |||||
| } | |||||
| if (bin !== null) { | |||||
| formData.append('bin', bin.toString()); | |||||
| } | |||||
| if (optionalEaResourceId !== null) { | |||||
| formData.append('optionalEaResourceId', optionalEaResourceId.toString()); | |||||
| } | |||||
| return this.httpClient.post( | |||||
| `${environment.apiUrl}/data-import/import-futwiz-player`, | |||||
| formData | |||||
| ); | |||||
| } | |||||
| eaDataImport() { | |||||
| return this.httpClient.get( | |||||
| `${environment.apiUrl}/data-import/import-ea-data` | |||||
| ); | |||||
| } | |||||
| futwizRaritiesImport() { | |||||
| return this.httpClient.get( | |||||
| `${environment.apiUrl}/data-import/import-futwiz-rarities` | |||||
| ); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,35 @@ | |||||
| import { Injectable } from '@angular/core'; | |||||
| import {HttpClient, HttpHeaders} from "@angular/common/http"; | |||||
| import {User} from "@app/_models"; | |||||
| import {environment} from "@environments/environment"; | |||||
| @Injectable({ | |||||
| providedIn: 'root' | |||||
| }) | |||||
| export class EaDataConnectService { | |||||
| constructor( | |||||
| private httpClient: HttpClient | |||||
| ) { | |||||
| } | |||||
| connectAccount(accountId: string) { | |||||
| const formData = new FormData(); | |||||
| formData.append('accountId', accountId.toString()); | |||||
| return this.httpClient.post( | |||||
| `${environment.apiUrl}/ea-data-connect/connect-account`, | |||||
| formData | |||||
| ); | |||||
| } | |||||
| deleteCachefile(accountId: string) { | |||||
| const formData = new FormData(); | |||||
| formData.append('accountId', accountId.toString()); | |||||
| return this.httpClient.post( | |||||
| `${environment.apiUrl}/ea-data-connect/delete-cache-file`, | |||||
| formData | |||||
| ); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,2 @@ | |||||
| export * from './account.service'; | |||||
| export * from './alert.service'; | |||||
| @@ -0,0 +1,20 @@ | |||||
| import { Injectable } from '@angular/core'; | |||||
| import { BehaviorSubject, Observable } from 'rxjs'; | |||||
| @Injectable({ | |||||
| providedIn: 'root' | |||||
| }) | |||||
| export class LoadingService { | |||||
| private loadingSubject: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false); | |||||
| public loading: Observable<boolean> = this.loadingSubject.asObservable(); | |||||
| constructor() { } | |||||
| setLoading(loading: boolean): void { | |||||
| this.loadingSubject.next(loading); | |||||
| } | |||||
| getLoading(): Observable<boolean> { | |||||
| return this.loading; | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,23 @@ | |||||
| import { Injectable } from '@angular/core'; | |||||
| import {HttpClient, HttpHeaders} from "@angular/common/http"; | |||||
| import {User} from "@app/_models"; | |||||
| import {environment} from "@environments/environment"; | |||||
| @Injectable({ | |||||
| providedIn: 'root' | |||||
| }) | |||||
| export class LogService { | |||||
| constructor( | |||||
| private httpClient: HttpClient | |||||
| ) { | |||||
| } | |||||
| getLogs(lines: number) { | |||||
| return this.httpClient.get( | |||||
| `${environment.apiUrl}/logs/logs/${lines}`, | |||||
| ); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,100 @@ | |||||
| import { Injectable } from '@angular/core'; | |||||
| import {HttpClient, HttpHeaders} from "@angular/common/http"; | |||||
| import {User} from "@app/_models"; | |||||
| import {environment} from "@environments/environment"; | |||||
| @Injectable({ | |||||
| providedIn: 'root' | |||||
| }) | |||||
| export class SnipingService { | |||||
| constructor( | |||||
| private httpClient: HttpClient | |||||
| ) { | |||||
| } | |||||
| snipingPrepare( | |||||
| accountIdsJson: number[] | |||||
| ) { | |||||
| const formData = new FormData(); | |||||
| formData.append('accountIdsJson', JSON.stringify(accountIdsJson)); | |||||
| return this.httpClient.post( | |||||
| `${environment.apiUrl}/sniping/prepare`, | |||||
| formData | |||||
| ); | |||||
| } | |||||
| snipingExecute( | |||||
| eaResourceId: number, | |||||
| snipeTimeMin: number, | |||||
| snipeTimeMax: number, | |||||
| buyItemsMax: number, | |||||
| buyBin: number, | |||||
| sellBin: number, | |||||
| accountIdsJson: number[], | |||||
| round: number, | |||||
| gapTimeMin: number, | |||||
| gapTimeMax: number, | |||||
| putOnTpOnly: boolean, | |||||
| priceCheckBin: number | undefined, | |||||
| doPriceCheck: boolean, | |||||
| ) { | |||||
| const formData = new FormData(); | |||||
| formData.append('eaResourceId', eaResourceId.toString()); | |||||
| formData.append('snipeTimeMin', snipeTimeMin.toString()); | |||||
| formData.append('snipeTimeMax', snipeTimeMax.toString()); | |||||
| formData.append('buyItemsMax', buyItemsMax.toString()); | |||||
| formData.append('buyBin', buyBin.toString()); | |||||
| formData.append('sellBin', sellBin.toString()); | |||||
| formData.append('accountIdsJson', JSON.stringify(accountIdsJson)); | |||||
| formData.append('round', round.toString()); | |||||
| formData.append('gapTimeMin', gapTimeMin.toString()); | |||||
| formData.append('gapTimeMax', gapTimeMax.toString()); | |||||
| formData.append('putOnTpOnly', putOnTpOnly ? "1" : "0"); | |||||
| if (priceCheckBin) { | |||||
| formData.append('priceCheckBin', priceCheckBin.toString()); | |||||
| } | |||||
| formData.append('doPriceCheck', doPriceCheck ? "1" : "0"); | |||||
| return this.httpClient.post( | |||||
| `${environment.apiUrl}/sniping/execute`, | |||||
| formData | |||||
| ); | |||||
| } | |||||
| snipingStop( | |||||
| accountIdsJson: number[] | |||||
| ) { | |||||
| const formData = new FormData(); | |||||
| formData.append('accountIdsJson', JSON.stringify(accountIdsJson)); | |||||
| return this.httpClient.post( | |||||
| `${environment.apiUrl}/sniping/stop`, | |||||
| formData | |||||
| ); | |||||
| } | |||||
| checkPrice( | |||||
| accountId: number, | |||||
| eaResourceId: number, | |||||
| priceCheckBin: number, | |||||
| ) { | |||||
| const formData = new FormData(); | |||||
| formData.append('accountId', accountId.toString()); | |||||
| formData.append('eaResourceId', eaResourceId.toString()); | |||||
| formData.append('priceCheckBin', priceCheckBin.toString()); | |||||
| return this.httpClient.post( | |||||
| `${environment.apiUrl}/sniping/check-bin-price`, | |||||
| formData | |||||
| ); | |||||
| } | |||||
| updateAccountSnipingCounts() { | |||||
| const formData = new FormData(); | |||||
| return this.httpClient.post( | |||||
| `${environment.apiUrl}/sniping/update-account-sniping-counts`, | |||||
| formData | |||||
| ); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,16 @@ | |||||
| import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms'; | |||||
| export function minMaxValidator(minKey: string, maxKey: string, errorKey: string): ValidatorFn { | |||||
| return (formGroup: AbstractControl): ValidationErrors | null => { | |||||
| const minControl = formGroup.get(minKey); | |||||
| const maxControl = formGroup.get(maxKey); | |||||
| if (minControl && maxControl && minControl.value > maxControl.value) { | |||||
| return { | |||||
| [errorKey]: { | |||||
| message: `${minKey} (${minControl.value}) must be less than or equal to ${maxKey} (${maxControl.value})` | |||||
| } | |||||
| }; | |||||
| } | |||||
| return null; | |||||
| }; | |||||
| } | |||||
| @@ -0,0 +1,22 @@ | |||||
| import { NgModule } from '@angular/core'; | |||||
| import { Routes, RouterModule } from '@angular/router'; | |||||
| import { LayoutComponent } from './layout.component'; | |||||
| import { LoginComponent } from './login.component'; | |||||
| import { RegisterComponent } from './register.component'; | |||||
| const routes: Routes = [ | |||||
| { | |||||
| path: '', component: LayoutComponent, | |||||
| children: [ | |||||
| { path: 'login', component: LoginComponent }, | |||||
| { path: 'register', component: RegisterComponent } | |||||
| ] | |||||
| } | |||||
| ]; | |||||
| @NgModule({ | |||||
| imports: [RouterModule.forChild(routes)], | |||||
| exports: [RouterModule] | |||||
| }) | |||||
| export class AccountRoutingModule { } | |||||
| @@ -0,0 +1,22 @@ | |||||
| import { NgModule } from '@angular/core'; | |||||
| import { ReactiveFormsModule } from '@angular/forms'; | |||||
| import { CommonModule } from '@angular/common'; | |||||
| import { AccountRoutingModule } from './account-routing.module'; | |||||
| import { LayoutComponent } from './layout.component'; | |||||
| import { LoginComponent } from './login.component'; | |||||
| import { RegisterComponent } from './register.component'; | |||||
| @NgModule({ | |||||
| imports: [ | |||||
| CommonModule, | |||||
| ReactiveFormsModule, | |||||
| AccountRoutingModule | |||||
| ], | |||||
| declarations: [ | |||||
| LayoutComponent, | |||||
| LoginComponent, | |||||
| RegisterComponent | |||||
| ] | |||||
| }) | |||||
| export class AccountModule { } | |||||
| @@ -0,0 +1,3 @@ | |||||
| <div class="container col-md-6 offset-md-3 mt-5"> | |||||
| <router-outlet></router-outlet> | |||||
| </div> | |||||
| @@ -0,0 +1,17 @@ | |||||
| import { Component } from '@angular/core'; | |||||
| import { Router } from '@angular/router'; | |||||
| import { AccountService } from '@app/_services'; | |||||
| @Component({ templateUrl: 'layout.component.html' }) | |||||
| export class LayoutComponent { | |||||
| constructor( | |||||
| private router: Router, | |||||
| private accountService: AccountService | |||||
| ) { | |||||
| // redirect to home if already logged in | |||||
| if (this.accountService.userValue) { | |||||
| this.router.navigate(['/']); | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,28 @@ | |||||
| <div class="card login-form"> | |||||
| <h1 class="card-header">Login</h1> | |||||
| <div class="card-body"> | |||||
| <form [formGroup]="form" (ngSubmit)="onSubmit()"> | |||||
| <div class="mb-3"> | |||||
| <label class="form-label">Username</label> | |||||
| <input type="text" formControlName="username" class="form-control" [ngClass]="{ 'is-invalid': submitted && f['username'].errors }" /> | |||||
| <div *ngIf="submitted && f['username'].errors" class="invalid-feedback"> | |||||
| <div *ngIf="f['username'].hasError('required')">Username is required</div> | |||||
| </div> | |||||
| </div> | |||||
| <div class="mb-3"> | |||||
| <label class="form-label">Password</label> | |||||
| <input type="password" formControlName="password" class="form-control" [ngClass]="{ 'is-invalid': submitted && f['password'].errors }" /> | |||||
| <div *ngIf="submitted && f['password'].errors" class="invalid-feedback"> | |||||
| <div *ngIf="f['password'].hasError('required')">Password is required</div> | |||||
| </div> | |||||
| </div> | |||||
| <div> | |||||
| <button [disabled]="loading" class="btn btn-primary"> | |||||
| <span *ngIf="loading" class="spinner-border spinner-border-sm me-1"></span> | |||||
| Login | |||||
| </button> | |||||
| <!-- <a routerLink="../register" class="btn btn-link">Register</a>--> | |||||
| </div> | |||||
| </form> | |||||
| </div> | |||||
| </div> | |||||
| @@ -0,0 +1,58 @@ | |||||
| import { Component, OnInit } from '@angular/core'; | |||||
| import { Router, ActivatedRoute } from '@angular/router'; | |||||
| import { FormBuilder, FormGroup, Validators } from '@angular/forms'; | |||||
| import { first } from 'rxjs/operators'; | |||||
| import { AccountService, AlertService } from '@app/_services'; | |||||
| @Component({ templateUrl: 'login.component.html' }) | |||||
| export class LoginComponent implements OnInit { | |||||
| form!: FormGroup; | |||||
| loading = false; | |||||
| submitted = false; | |||||
| constructor( | |||||
| private formBuilder: FormBuilder, | |||||
| private route: ActivatedRoute, | |||||
| private router: Router, | |||||
| private accountService: AccountService, | |||||
| private alertService: AlertService | |||||
| ) { } | |||||
| ngOnInit() { | |||||
| this.form = this.formBuilder.group({ | |||||
| username: ['', Validators.required], | |||||
| password: ['', Validators.required] | |||||
| }); | |||||
| } | |||||
| // convenience getter for easy access to form fields | |||||
| get f() { return this.form.controls; } | |||||
| onSubmit() { | |||||
| this.submitted = true; | |||||
| // reset alerts on submit | |||||
| this.alertService.clear(); | |||||
| // stop here if form is invalid | |||||
| if (this.form.invalid) { | |||||
| return; | |||||
| } | |||||
| this.loading = true; | |||||
| this.accountService.login(this.f['username'].value, this.f['password'].value) | |||||
| .pipe(first()) | |||||
| .subscribe({ | |||||
| next: () => { | |||||
| // get return url from query parameters or default to home page | |||||
| const returnUrl = this.route.snapshot.queryParams['returnUrl'] || '/'; | |||||
| this.router.navigateByUrl(returnUrl); | |||||
| }, | |||||
| error: error => { | |||||
| this.alertService.error(error); | |||||
| this.loading = false; | |||||
| } | |||||
| }); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,43 @@ | |||||
| <div class="card"> | |||||
| <h4 class="card-header">Register</h4> | |||||
| <div class="card-body"> | |||||
| <form [formGroup]="form" (ngSubmit)="onSubmit()"> | |||||
| <div class="mb-3"> | |||||
| <label class="form-label">First Name</label> | |||||
| <input type="text" formControlName="firstName" class="form-control" [ngClass]="{ 'is-invalid': submitted && f['firstName'].errors }" /> | |||||
| <div *ngIf="submitted && f['firstName'].errors" class="invalid-feedback"> | |||||
| <div *ngIf="f['firstName'].hasError('required')">First Name is required</div> | |||||
| </div> | |||||
| </div> | |||||
| <div class="mb-3"> | |||||
| <label class="form-label">Last Name</label> | |||||
| <input type="text" formControlName="lastName" class="form-control" [ngClass]="{ 'is-invalid': submitted && f['lastName'].errors }" /> | |||||
| <div *ngIf="submitted && f['lastName'].errors" class="invalid-feedback"> | |||||
| <div *ngIf="f['lastName'].hasError('required')">Last Name is required</div> | |||||
| </div> | |||||
| </div> | |||||
| <div class="mb-3"> | |||||
| <label class="form-label">Username</label> | |||||
| <input type="text" formControlName="username" class="form-control" [ngClass]="{ 'is-invalid': submitted && f['username'].errors }" /> | |||||
| <div *ngIf="submitted && f['username'].errors" class="invalid-feedback"> | |||||
| <div *ngIf="f['username'].hasError('required')">Username is required</div> | |||||
| </div> | |||||
| </div> | |||||
| <div class="mb-3"> | |||||
| <label class="form-label">Password</label> | |||||
| <input type="password" formControlName="password" class="form-control" [ngClass]="{ 'is-invalid': submitted && f['password'].errors }" /> | |||||
| <div *ngIf="submitted && f['password'].errors" class="invalid-feedback"> | |||||
| <div *ngIf="f['password'].hasError('required')">Password is required</div> | |||||
| <div *ngIf="f['password'].hasError('minlength')">Password must be at least 6 characters</div> | |||||
| </div> | |||||
| </div> | |||||
| <div> | |||||
| <button [disabled]="loading" class="btn btn-primary"> | |||||
| <span *ngIf="loading" class="spinner-border spinner-border-sm me-1"></span> | |||||
| Register | |||||
| </button> | |||||
| <a routerLink="../login" class="btn btn-link">Cancel</a> | |||||
| </div> | |||||
| </form> | |||||
| </div> | |||||
| </div> | |||||
| @@ -0,0 +1,59 @@ | |||||
| import { Component, OnInit } from '@angular/core'; | |||||
| import { Router, ActivatedRoute } from '@angular/router'; | |||||
| import { FormBuilder, FormGroup, Validators } from '@angular/forms'; | |||||
| import { first } from 'rxjs/operators'; | |||||
| import { AccountService, AlertService } from '@app/_services'; | |||||
| @Component({ templateUrl: 'register.component.html' }) | |||||
| export class RegisterComponent implements OnInit { | |||||
| form!: FormGroup; | |||||
| loading = false; | |||||
| submitted = false; | |||||
| constructor( | |||||
| private formBuilder: FormBuilder, | |||||
| private route: ActivatedRoute, | |||||
| private router: Router, | |||||
| private accountService: AccountService, | |||||
| private alertService: AlertService | |||||
| ) { } | |||||
| ngOnInit() { | |||||
| this.form = this.formBuilder.group({ | |||||
| firstName: ['', Validators.required], | |||||
| lastName: ['', Validators.required], | |||||
| username: ['', Validators.required], | |||||
| password: ['', [Validators.required, Validators.minLength(6)]] | |||||
| }); | |||||
| } | |||||
| // convenience getter for easy access to form fields | |||||
| get f() { return this.form.controls; } | |||||
| onSubmit() { | |||||
| this.submitted = true; | |||||
| // reset alerts on submit | |||||
| this.alertService.clear(); | |||||
| // stop here if form is invalid | |||||
| if (this.form.invalid) { | |||||
| return; | |||||
| } | |||||
| this.loading = true; | |||||
| this.accountService.register(this.form.value) | |||||
| .pipe(first()) | |||||
| .subscribe({ | |||||
| next: () => { | |||||
| this.alertService.success('Registration successful', { keepAfterRouteChange: true }); | |||||
| this.router.navigate(['../login'], { relativeTo: this.route }); | |||||
| }, | |||||
| error: error => { | |||||
| this.alertService.error(error); | |||||
| this.loading = false; | |||||
| } | |||||
| }); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,13 @@ | |||||
| <div class="spt-container"> | |||||
| <div class="d-flex justify-content-between align-items-start"> | |||||
| <h2>{{ 'dashboard.view' | translate }}</h2> | |||||
| </div> | |||||
| <mat-tab-group> | |||||
| <mat-tab label="{{ 'dashboard.overview' | translate }}"> | |||||
| OVERVIEW | |||||
| </mat-tab> | |||||
| <mat-tab label="{{ 'dashboard.overview' | translate }}"> | |||||
| OVERVIEW 2 | |||||
| </mat-tab> | |||||
| </mat-tab-group> | |||||
| </div> | |||||
| @@ -0,0 +1,5 @@ | |||||
| pre { | |||||
| white-space: pre-wrap; /* Erlaubt Zeilenumbrüche */ | |||||
| word-wrap: break-word; /* Bricht lange Wörter um */ | |||||
| overflow-wrap: break-word; /* Sicherstellt, dass Wörter, die zu lang sind, auch umgebrochen werden */ | |||||
| } | |||||