| @@ -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 */ | |||
| } | |||